Survey Solutions for SurveyCTO Power Users
A practical, operations-focused masterclass for experienced SurveyCTO programmers moving into Survey Solutions questionnaire design, fieldwork management, supervision, exports, quality assurance and integrations.
How to use this course
Build one questionnaire as you learn. Open the synthetic Practice PDF from the top bar and keep it beside the lesson. Short build-along prompts start in Module 4; by Module 14, you will be finishing and testing a questionnaire you have already built in stages. No private Designer account or shared questionnaire is needed.
This is a self-paced course. You can stop and return on the same browser; module access, quiz attempts and progress are stored locally on this device. After completing the course once, use the sidebar as a reference manual.
XLSForm logic, repeats, relevance, constraints, preloads, field data pipelines.
Designer hierarchy, C# expressions, rosters, linked questions, assignments and interview states.
A mental model strong enough to build, deploy, supervise, export and troubleshoot a real project.
What exactly is Survey Solutions?
You already know how to build surveys. This module is about understanding the product you are moving into: what it is, why organizations use it, which account you actually need, and what is different from simply signing up for SurveyCTO.
Survey Solutions in one sentence
Survey Solutions is a free, open-source platform developed by the World Bank for designing, implementing, managing, and monitoring large-scale surveys and censuses.
It supports interviewer-administered CAPI and web interviewing while integrating questionnaire programming, field assignments, supervision, interview review, paradata, exports, and APIs.
Why would a project choose Survey Solutions?
It is not universally βbetterβ than SurveyCTO, ODK, KoboToolbox, or other platforms. Its strengths are especially relevant when a project has a formal field hierarchy, complex household or establishment instruments, large samples, strong supervision requirements, or institutional requirements around World Bank-supported workflows.
Assignments, supervisors, interviewers, approval/rejection workflows, sample identifiers and interview status are core objects rather than optional add-ons.
Android interviewers can work offline and synchronize when connectivity is available, which suits fieldwork in low-connectivity settings.
The system records much more than final answers: interview actions, timing, synchronization and review history can support field monitoring and quality assurance.
Organizations can run Survey Solutions on their own infrastructure or cloud environment, which can matter for data governance and institutional control.
The visual Designer handles standard survey logic, while C# expressions, rosters, linked questions, calculated variables and lookup tables support advanced instruments.
Survey Solutions servers expose REST and GraphQL APIs, allowing teams to connect assignments, exports, monitoring systems and external databases.
Where SurveyCTO may still feel easier to you
| Task | SurveyCTO experience | Survey Solutions experience |
|---|---|---|
| Questionnaire authoring | XLSForm in Google Sheets is fast for power users and easy to bulk-edit. | Designer is hierarchical and browser-based; there is no native XLSForm workflow. |
| Shared server-side reference data | Server datasets, publishing and pulldata() are highly flexible. | Static lookup tables, assignments and APIs cover different parts of that job; there is no single 1:1 server-dataset equivalent. |
| Operational hierarchy | Flexible user/group patterns can be lightweight. | HQ β Supervisor β Interviewer is much more explicit and central to the system. |
| Hosting | Usually feels like a managed SaaS service to project teams. | A project may use an organization-managed Survey Solutions server or its own hosted server, so server administration can be more visible. |
Accounts: the first thing that confuses new users
Your Designer account and your project data-server account are not the same account. This distinction is fundamental.
This is your personal account on the online Questionnaire Designer. You use it to create, collaborate on, test and manage questionnaire source documents.
Registration:
- Go to
designer.mysurvey.solutions. - Choose Register.
- Create a login, enter your full name and email, and set a password.
- Confirm the registration from the email sent to you.
- Sign in; your starting workspace is My Questionnaires.
This is an account created on a specific Survey Solutions server for actual survey operations. Its role may be Administrator, Headquarters, Supervisor, Interviewer, Observer or API User.
Important: creating a Designer account does not automatically give you access to your project's Headquarters server. The project's server administrator or HQ team must create or assign the appropriate server account.
Your recommended setup for this course
- Create a Designer account first. This is where you will program the questionnaire.
- Ask the incoming project for the Survey Solutions server URL and your intended role. If they already have a Survey Solutions server, use the server and access details provided by the project.
- For training, request a Personal Demo Server (PDS) through the Survey Solutions self-service/PDS portal using your Designer credentials. A PDS is a temporary practice server intended for learning, not real data collection.
- Install the Android Tester or Interviewer tools only when needed. You can learn a lot in Designer and the web interface before touching a field device.
A simple picture of the identities involved
YOU
β
ββ Designer account
β ββ create / collaborate / test questionnaires
β
ββ Project data-server account
ββ Administrator
ββ Headquarters
ββ Supervisor
ββ Interviewer
ββ Observer
ββ API User
Designer account β automatic access to project serverBefore you continue: make sure you can answer these
- What is the difference between Designer and Headquarters?
- Does registering for Designer automatically create an HQ account on your project's server?
- Would you use a Personal Demo Server for real respondent data?
- Name two reasons a project might choose Survey Solutions other than βbecause the World Bank developed it.β
Show model answers
1. Designer is where questionnaire source documents are created; Headquarters is part of a data server used to deploy and manage real survey operations. 2. No. A server administrator or HQ team creates the relevant server account. 3. No; PDS is for learning and testing. 4. Examples: built-in supervision, offline CAPI, paradata/audit, self-hosting/data control, complex roster-based instruments, or API integration.
Official sources used for this orientation
The course now also includes an embedded component screenshot in the Architecture module so you can connect the written explanations to an actual Survey Solutions visual.
Current documentation checked September 2026. Open the sources directly:
How your Designer questionnaire gets onto the project server
This is the Survey Solutions step that is closest to SurveyCTO's Upload form definition. The idea is familiar, but the transfer is initiated from Headquarters.
Google Sheet / XLSForm
β
Upload form definition
β
SurveyCTO server
β
Collect / web form
Questionnaire in Designer
β
HQ imports questionnaire
β
Project data server
β
Assignments
β
Interviewer / Web Interviewer
Set up your Personal Demo Server before the practical modules
A Personal Demo Server (PDS) gives you Administrator and Headquarters controls for a private practice environment. Use it for the course lab because the public demonstration server is mainly for observing existing content and cannot provide the same controlled setup.
- Create and confirm your Designer account first.
- Open pds.mysurvey.solutions and sign in through the self-service portal.
- Submit a Personal Demo Server request. Each user may have one active PDS.
- Wait for the creation notice and server credentials. The normal address follows
https://username-demo.mysurvey.solutions. - Store the PDS address and administrator credential in your password manager. They are separate from your Designer credentials.
- The server and all its data are automatically deleted after 30 days.
- The period cannot be extended; request a new PDS after expiry.
- There is no backup or recovery service.
- Use only synthetic practice records. Never load respondent data.
- The environment is sized for learning and small tests, approximately 1,000 interviews.
Create the course practice team
Sign in to the PDS as its administrator and create these synthetic accounts. Choose unique temporary passwords and do not reuse a real work password.
| Practice login | Role | Purpose in the lab |
|---|---|---|
hq_train | Headquarters | Import the questionnaire, upload assignments, review interviews and export data. |
sup_train | Supervisor | Receive the batch of assignments, allocate cases, review, reject and approve. |
enum_01 β Mike Johnson | Interviewer under sup_train | Receive directly assigned cases, synchronize the Android app, conduct interviews and correct rejected work. |
enum_02 β Abel Kayembe | Interviewer under sup_train | Receive directly assigned cases, run a connected Web Interviewer test and compare it with Android. |
Create users in the current PDS interface
- Open the administrator area, select Users, and choose ADD USER for one account or UPLOAD USERS for a tab-delimited batch.
- For a manual account, select the workspace and role, enter the login, enter the compliant password twice, then optionally add the full name, email and phone number.
- Create
sup_trainbefore creating or uploading interviewers whosesupervisorcolumn containssup_train.
Batch-user dependency check: create the supervisor first
sup_train, but that supervisor did not yet exist in the selected workspace. Create the supervisor first or include a valid supervisor account in the batch as supported by the current template.sup_train. Sign out between roles or use separate browser profiles to avoid confusing sessions.The practice deployment cycle
Build the practice questionnaire, compile with zero errors, then test the main branches.
Import the Designer questionnaire as Version 1.
Upload the embedded tab-delimited household list. Its
_responsible values route cases directly to enum_01 and enum_02.Monitor the two interviewers, review submitted cases, reject one practice case for correction, and approve the corrected work.
Synchronize or open the web dashboard, execute test scenarios, complete and submit.
Reject, correct, resubmit, approve, export and inspect paradata before the PDS expires.
Personal Demo Server guidance β
First version: step by step
- Build and test the questionnaire in Designer and resolve questionnaire errors.
- Make sure the Designer account that will be used for import has access to the questionnaire. The owner can share it with another Designer user.
- Sign in to the project's Survey Solutions server as Headquarters or Administrator.
- Go to Survey Setup β Questionnaires β Import questionnaire.
- Headquarters asks you to sign in to Designer. These are Designer credentials, not the project-server credentials.
- HQ lists the questionnaires that Designer account can access.
- Select the questionnaire and confirm the import.
- The questionnaire appears on the project server as Version 1.
- Create assignments from that imported questionnaire and assign them to the appropriate field team.
Real PDS import sequence
So how are the two accounts linked?
They are connected at the moment of import rather than through one permanent shared login:
Signed in to project server as HQ
β
HQ asks for Designer login
β
Designer checks questionnaires you can access
β
You select questionnaire
β
HQ copies it onto the project serverThis is why the same person may have a Designer login and a different project-server login.
What happens when you update the questionnaire?
This is the closest equivalent of uploading a revised XLSForm in SurveyCTO.
- Edit the XLSForm.
- Upload the revised form definition.
- The server receives the new form version.
- Edit the questionnaire in Designer.
- Test it and resolve errors.
- In HQ, import the same questionnaire again.
- The project server stores it as the next questionnaire version.
- Decide whether eligible unused assignments should move to that newer version.
Versioning
Household Survey Version 1 imported Monday Version 2 imported Wednesday Version 3 imported Friday
Multiple versions can exist on the same project server. New assignments can be created from a selected imported version, although normally you would use the latest tested version.
What about assignments that have not started?
When a newer questionnaire version is imported, Survey Solutions can offer to upgrade eligible assignments. If an assignment can be moved, the old assignment is archived and the remaining workload is recreated using the newer questionnaire version. Started interviews are not converted.
SurveyCTO comparison
| Task | SurveyCTO | Survey Solutions |
|---|---|---|
| Create/edit questionnaire | Usually Google Sheets / XLSForm | Questionnaire Designer |
| Put first version on server | Upload form definition | HQ imports questionnaire from Designer |
| Update questionnaire | Edit XLSForm and upload again | Edit in Designer and import again |
| Does editing source automatically update server? | No | No |
| Questionnaire versions on server | Form versions | Imported questionnaire versions |
| Move unused work to newer version | Handled through your form/case workflow | Upgrade eligible assignments |
Who can import?
The import is performed from Headquarters. The Designer login used during import must have access to the questionnaire. A collaborator with either View or Edit access in Designer can import that questionnaire to Headquarters.
Common reasons an import fails
- The questionnaire still contains errors in Designer.
- The Designer account used during import does not have access to the questionnaire.
- The project server cannot reach
designer.mysurvey.solutions. - The questionnaire uses a feature that the version of Survey Solutions on the project server does not yet support.
- The Designer and project-server software versions are incompatible.
You change a validation rule in Designer on Wednesday. Enumerators are already working with Version 1, while 40 assignments have not yet started. What happens?
Show model answer
Test the change in Designer, then import the questionnaire again from HQ to create the next version. Interviews already started with Version 1 remain on Version 1. Eligible assignments that have not started can be moved to the newer version using the assignment-upgrade process. Survey Solutions recommends minimizing questionnaire changes once fieldwork has begun.
Official documentation for this module
- Request and use a Personal Demo Server
- Import questionnaires and manage versions in Headquarters
- Share a questionnaire in Designer
- Updating questionnaires after fieldwork has started
- How Headquarters communicates with Designer
- Troubleshoot Designer-to-server import errors
- Create assignments from an imported questionnaire version
Architecture: stop thinking βform uploadβ
The first job is to replace your SurveyCTO mental model with the Survey Solutions operational model.
Big-picture component flow
Documentation gallery: what the system looks like
The official documentation site blocks some images from being displayed inside a separate local HTML file. To make the course reliable offline, the gallery below uses embedded visual guides recreated from the official documentation workflows. They are not pixel-for-pixel screenshots, but they show the same screens, controls, and steps without depending on external images.
| Name | Version | Imported |
|---|---|---|
| Household Survey | 3 | 18 Sep |
| Listing | 2 | 17 Sep |
| Community Survey | 1 | 15 Sep |
Headquarters β questionnaire list
Imported questionnaires and versions appear here. This is the server-side counterpart to SurveyCTO's list of uploaded form definitions.
Open the official documentation βHQ connecting to Designer
HQ asks for Designer credentials during import. That temporary connection lets HQ see the questionnaires that Designer account can access.
Read the current import instructions β| # | Responsible | Quantity | Status |
|---|---|---|---|
| 1021 | sup_linda | 25 | Assigned |
| 1022 | int_maria | 1 | Received |
| 1023 | hq_manager | 40 | Assigned |
Headquarters β assignments
Assignments show who is responsible for specific work, the questionnaire version, quantity, and identifying information.
Open the assignments documentation βWeb Tester β questionnaire preview
Use the browser-based tester to check wording, enabling conditions, validations, rosters, and navigation before fieldwork.
Open the Web Tester documentation βDesigner β launching a test
After questionnaire errors are resolved, the Test control launches the questionnaire for browser testing.
See the official testing steps βmodified today
modified yesterday
modified 3 days ago
Selecting a Designer questionnaire for import
After authentication, HQ shows questionnaires that your Designer account can access. You choose which questionnaire to copy to the project server.
Open the current import guide βGoogle Sheet / XLSForm
β
SurveyCTO server
β
Collect
β
submissionQuestionnaire Designer
β
Headquarters
β
Assignment
β
Interviewer
β
Interview
β
Review / approval
β
ExportCore actors
| Survey Solutions component | Think of it as | What matters |
|---|---|---|
| Designer | Your XLSForm authoring environment | Questionnaire structure + C# logic |
| Headquarters (HQ) | Server + operations console | Questionnaires, assignments, monitoring, review, exports |
| Supervisor | Built-in field manager tier | Team workload, review, reassignment |
| Interviewer | Collect user | Receives work, conducts interviews, syncs |
int_07 says βmy blank form is missing.β Known objects: questionnaire HH_Survey_v3, assignment A-041, and user int_07. Reframe the problem by checking questionnaire version, assignment responsibility, and synchronization.Designer: create, edit, test, and collaborate
Build your own questionnaire first, then learn to collaborate. Work through parts 4Aβ4F with Designer open in a second tab. Modules 5β8 extend these skills with expressions, calculations, rosters, and linked questions.
- 4A. Create your questionnaire
- 4B. Edit questions and organize sections
- 4C. Categories and questionnaire tools
- 4D. Compile, test, and revise
- 4E. Share with colleagues
- 4F. Receive a shared questionnaire
Allow approximately 2β3 hours including hands-on practice, within the course's overall practice allocation.
4A. Create your first questionnaire
Sign in to Designer. On its home page choose Create new, name the instrument Household practice β your name, and select Create. Reopen it later by selecting its title in your questionnaire list. Designer home page β
Open Settings, enter a questionnaire variable such as hh_practice, and Save. This variable names the main exported data file; it is different from the visible questionnaire title and from each question's variable name. Questionnaire settings β
Plan your sections before adding questions. The Cover holds deliberately chosen identifying information; interview content belongs in sections such as Consent and Household. Add these sections using the section controls. The tree below illustrates how the instrument will grow; you will learn roster construction in Module 7.
Questionnaire ββ Cover β ββ enumeration_area β ββ household_id ββ Section: Consent ββ Section: Household β ββ hhsize β ββ Roster: members β ββ name β ββ age β ββ sex ββ Section: Remittances ββ ...
| SurveyCTO / XLSForm | Survey Solutions |
|---|---|
begin group | Section / sub-section |
begin repeat | Roster |
calculate | Calculated variable |
note | Static text |
relevance | Enabling condition |
constraint | Validation condition |
required=yes | Critical questions or rules for submission requirements; see Module 6 for the difference from navigation blocking |
4B. Edit questions and organize sections
The tree selects what you work on; the right editor changes that element; the narrow left rail opens questionnaire-wide tools. Start by building a small instrument you understand.
- Select Consent and choose Add question. Create a categorical single-select named
consentwith text βMay we begin this practice interview?β and question-specific options1 = Yesand2 = No. Save. - Select Household. Add a Text question named
respondent_name, then an Integer numeric question namedrespondent_age. Give each clear question text and Save. - Select a question in the tree to reopen it. Question text is the interview wording; variable name identifies it in expressions and exports; variable label provides descriptive metadata. Change wording, then Save. Cancel discards an unsaved edit.
- Add interviewer instructions for procedural guidance and Static text for information that needs no answer. Calculated variables are introduced in Module 6.
- Drag to reorder within a section. To move to another section, select the element and use Move to. Review its new context, especially if moving into or out of a roster. Use Search for question to locate questions in a large instrument.
- Before deleting or renaming an element, inspect references to it. After copying an element, review its name, categories and logic. Compile after structural changes.
Keep this first build simple. Modules 5β6 teach enabling and validation expressions; at this stage you are mastering the editor. Elements and ordering β
Variable name, variable label, and question text
These three properties describe the same question for different purposes. The name identifies its data; the label describes that data; the question text supplies the interview wording.
| Property | Purpose | Required or optional? | Example |
|---|---|---|---|
| Variable name | Identifier used by expressions and exports. | Required for a question; unique and subject to naming rules. | member_age |
| Variable label | Short description associated with the exported variable in statistical software. | Optional for ordinary questions; required for every Cover question. If omitted, question text supplies the export label. | Age in completed years |
| Question text | Wording presented during the interview, including supported substitutions. | Supply nonempty text for questions; Designer reports an empty title as WB0269. A variable label does not replace it. | How old is %member_name% in completed years? |
Ordinary variable labels are not shown on the tablet; identifying-question labels on the Cover are an exception. Question properties β
SurveyCTO reference: the XLSForm name corresponds to the variable name, while the respondent-facing label corresponds to Question text. Do not map SurveyCTO's label to Designer's Variable label merely because both use the word βlabel.β
When should they be similarβand when should they differ?
| Use case | Variable name | Variable label | Question text |
|---|---|---|---|
| Short identification field on the Cover | household_id | Household ID | Household ID |
| Roster question with a changing member name | member_age | Age in completed years | How old is %member_name% in completed years? |
| Amount with a defined reference period | food_spend_7d | Food expenditure, past 7 days (PHP) | During the past seven days, how much did your household spend on food, in pesos? |
Identical short wording can work for identifiers. For longer questions, write a concise analytical label that preserves units, population and reference period, while keeping natural interview wording in Question text. Put procedural guidance in Interviewer instruction, rather than burying it in an export label. In multilingual instruments, review translated question wording separately from the team's analytical naming convention.
%member_name% in the age question, but keep the label βAge in completed years.β Designer disallows substitutions in variable labels (WB0008); missing Cover labels produce WB0309. These authoring requirements do not make an interview answer compulsory: critical-question/rule settings and HQ treatment govern submission requirements. Designer messages βPublished limits are 80 characters for a variable label and 2,000 for question text. Keep both concise and check the exported statistical dataset rather than assuming a CSV header carries a variable label. Length limits β
respondent_age the label βRespondent age (completed years)β and full interview wording as Question text. Compile and Test. Inspect the label after a practice export. Then inspect household_id on the Cover and confirm it has a label. Changing a label or question wording does not rename the variable used by expressions.Variable names
Questions, calculated variables and rosters use identifiers that are referenced directly in expressions and appear in exports. Build a naming convention early, just as you would in XLSForm.
hh_size hh_member_age remit_received sender_country
Formatting question and static text
Survey Solutions allows a limited set of HTML tags inside question text and static text. This is the closest equivalent to using formatting markup in SurveyCTO labels. It is not arbitrary CSS: unsupported tags are removed by Designer. Formatting text β
| You want | Survey Solutions markup | Example |
|---|---|---|
| Bold / strong emphasis | <strong>...</strong> | <strong>Important</strong> |
| Italics | <i>...</i> or <em>...</em> | <i>Read aloud</i> |
| Underline | <u>...</u> | <u>Do not skip</u> |
| Line break | <br> | Line 1<br>Line 2 |
| Blue text | <font color="blue">...</font> | <font color="blue">Enumerator note</font> |
| Custom color | <font color="#2E4085">...</font> | Useful when you want an exact brand color. |
| Larger / smaller text | <big>, <small>, or <font size="..."> | Test carefully across tablet and web. |
| Subscript / superscript | <sub> / <sup> | m<sup>2</sup> |
Transfer recipient: <strong><font color="#2E4085">%recipient_name%</font></strong> <i>Please confirm the name before continuing.</i>
The value of %recipient_name% is piped into the formatted text. Survey Solutions uses the %variable% syntax for text substitution. Text substitution β
4C. Categories and questionnaire-wide tools
Reusable categories are named sets of numeric codes and labels stored at questionnaire level. The same set can serve more than one categorical question. This practice reference uses a synthetic education example; you create it yourself in Designer.
Create grade_codes in your own practice questionnaire
Open Reusable categories from Designer's left rail, then choose ADD NEW CATEGORIES (or the equivalent upload command in your version). The PDF lists the complete synthetic code set. For a first test, enter the rows below, save, and bind a categorical question's Source of categories to Reusable categories β grade_codes.
| value | title | Use |
|---|---|---|
| 10 | Kindergarten | Current and highest grade |
| 11 | Grade 1 | Current and highest grade |
| 12 | Grade 2 | Current and highest grade |
| 0 | No grade completed | Highest grade only |
| 60 | Post-graduate | Highest grade only |
| 99 | Don't know | Current and highest grade |
These are examples, not an installed list. Complete the remaining grade codes from the practice questionnaire's education-code table before the capstone. In a numeric category, write 0, not a text code 00; record that display label separately.
Where the set lives and what a change affects
- In your practice questionnaire, open Reusable categories in the far-left panel of advanced instruments.
- Create
grade_codesmanually or download Designer's XLSX/TAB template and upload a tab-delimited list withvalueandtitleheaders. - Bind
current_gradeandhighest_gradeto that set. Filter out codes that are valid only for highest completed grade from the current-enrollment question. - Save, compile, and test both bound questions. If changing an existing set, download a backup first: an upload replaces its categories.
The SurveyCTO connection: choice lists versus preloaded data
The closest everyday analogy is a named list in SurveyCTO's choices sheet: several select questions can use the same list. Uploading a category file is also useful for the long lists that SurveyCTO programmers often maintain externally.
SurveyCTO can load choices during an interview from an attached CSV or dataset using search(). A Designer category upload instead incorporates a static list into the questionnaire. It does not establish a live connection to the original spreadsheet or reproduce SurveyCTO's dataset filtering automatically. SurveyCTO preloaded choice lists β
pulldata() calls for a separate data/logic design; supplying a particular household's existing answers calls for assignment preloading (Module 9). Uploading reusable categories does not prefill answers or create assignments.When adapting a SurveyCTO list, map its choice-value column to value and label column to title. Designer requires numeric category codes: preserve a documented crosswalk for string or leading-zero identifiers rather than silently converting them. Rebuild and test dependent filtering explicitly. Subsequent category changes require the intended updated questionnaire version to reach HQ; editing the source file alone does not update deployed interviews.
Do not confuse four category and reference mechanisms
| Mechanism | Where it lives | Use it for | Editing consequence |
|---|---|---|---|
| Question-specific categories | Inside one categorical question | A short list used only by that question | The edit affects that question. |
| Reusable categories | Reusable Categories tool in this questionnaire | One coded answer list shared by several single- or multi-select questions; optionally cascading | The edit affects every question bound to that set. |
| Classification library | Designer library used to copy standardized classifications | Finding and inserting a maintained classification into a questionnaire | The inserted classification is a copy; later edits in the questionnaire do not update the library. |
| Lookup table | Lookup Tables tool in this questionnaire | Static numeric reference data used in expressions | It supplies values to logic; it is not the displayed answer list for a categorical question. |
The complete left sidebar: Panel of Advanced Instruments
From top to bottom, after the Designer logo, the rail in the screenshot opens these ten tools. Icon artwork can change between releases, so learn the tool name and purpose as well as its position. Panel of Advanced Instruments β
| Order / visual cue | Tool | What it opens | Typical trainee task |
|---|---|---|---|
| 1 Β· three horizontal lines | Table of contents | The questionnaire tree and navigation through sections and elements. | Jump to a section, roster, question, static text, or variable. |
| 2 Β· information circle | Questionnaire description and survey information | Questionnaire metadata and descriptive information. | Understand the instrument's purpose and documentation before editing. |
| 3 Β· language characters | Translations | Languages and translated questionnaire text. | Add or upload translations and check whether category labels are complete in every language. |
| 4 Β· stacked category cards | Reusable categories | Questionnaire-level coded category sets such as the synthetic grade_codes. | Edit a shared list once, then test all bound categorical questions. |
| 5 Β· branching path | Scenarios | Saved testing scenarios for questionnaire paths. | Re-run a known interview path after logic changes. |
6 Β· $m | Macros | Named reusable fragments used in expressions. | Inspect shared logic before changing a macro that may affect many expressions. |
| 7 Β· book/table | Lookup tables | Static numeric reference tables available to expressions. | Maintain numeric parameters or mappings used by calculations and validation. |
| 8 Β· paperclip | Attachments | Files attached to the questionnaire. | Manage images, documents, or category attachments referenced by the instrument. |
| 9 Β· speech bubbles | Comments | Designer collaboration comments. | Review open discussions and document a proposed or completed edit. |
| 10 Β· warning triangle | Critical rules | Questionnaire-level conditions checked at submission. | Understand why an interview cannot be submitted and preserve submission rules during edits. |
4D. Compile, test, and revise
Use the cycle edit β Save β Compile β Test β revise. Saving preserves an edit; compilation checks the questionnaire structure and expressions; testing checks the interview experience. A successful compile does not establish that the survey logic is correct.
- Compile the instrument. Open reported issues, fix their causes, save and compile again. Review warnings too.
- Choose Test and enter synthetic answers. Check wording, options, ordering, changing an answer, and returning to a question. Consent does not yet skip Household: you will add that logic in Modules 5β6.
- Record expected and observed results. Later, repeat tests for both consent outcomes, missing values and invalid ages after adding logic.
- Inspect your own revisions in History. Extract a revision into an independent copy for experiments; reverting changes the working document.
4E. Share your questionnaire with colleagues
Once your draft is ready for review, open Settings β Access. Enter your colleagueβs exact registered Designer email or login, choose View or Edit, and select Invite. They must already have a Designer account. View permits inspection; Edit permits changes and Designer comments.
Sharing gives access to the same document; copying creates an independent instrument. Agree who edits which sections and use History to follow changes. An editor can share further. To change View to Edit, revoke access and invite again with Edit. Ownership transfer is separate from sharing. Sharing controls β
4F. When someone shares their questionnaire with you
Others can also share their instruments with you. Open the questionnaire from your Designer list or their link. The editor, categories and testing tools work as described above. What changes is your permission, responsibility to the owner, and need to understand an existing instrument.
| How it was shared | What you can do | What to do when you need to edit |
|---|---|---|
| Edit access | Add, remove, and revise questionnaire elements and logic; work with Designer comments; test and compile. | Edit only after checking History and the team's version protocol. Your changes affect the shared questionnaire. |
| View access | Inspect, test, preview, view history, and make your own copy, but not modify the shared questionnaire or use its comments. | Ask the owner to revoke View access and share again with Edit access, or create a copy when the goal is practice. |
| Anonymous link | Read-only viewing and testing. An authenticated Designer user can create a personal copy. | Sign in and copy it, or ask the owner to share directly with your Designer account. |
- Confirm access and ownership. Open Settings β Access. If Access is unavailable, confirm your role with the owner. An inactive Save button alone does not prove read-only access: there may be no unsaved edits or required properties may be incomplete.
- Confirm the purpose. Determine whether this is a practice copy, the team's working questionnaire, or a version already imported into Headquarters.
- Read before editing. Open History to see who changed what and when. Use an earlier revision for comparison; use Extract when you need a separate copy from that point.
- Compile the starting state. Record existing errors and warnings so that you do not attribute old problems to your edit.
- Trace dependencies. Search the variable name and inspect enabling conditions, validations, substitutions, rosters, filters, macros, reusable categories, and critical rules that may depend on it.
- Make one coherent change. Save it, compile again, and test the affected route. For category changes, test every question bound to the same list.
- Leave a usable trail. Add a Designer comment or follow the project change log. Editing Designer does not update Headquarters automatically; HQ must import the intended questionnaire version.
caseid to Cover, then create the synthetic grade_codes reusable set from the Practice PDF. Save and compile before moving on. Reading and writing Survey Solutions expressions
This module builds the expression language from the ground up. By the end, you should be able to identify what an expression must return, recognize the type of each value, read a roster lambda such as person => person.age >= 18, and diagnose the most common logic errors.
${references}. Survey Solutions expressions use C# syntax and refer to questionnaire variable names directly. You are not expected to learn all of C#; you need the small, survey-specific subset taught here.1. Start with the job of the expression
Before writing syntax, ask what Designer expects the expression to produce. Most logic errors begin when an expression returns the wrong kind of result.
| Where you are writing | Question to ask | Required result | Example |
|---|---|---|---|
| Enabling condition | Should this element be active? | Boolean: true or false | consent == 1 |
| Validation condition | Is this answer acceptable? | Boolean | self >= 0 && self <= 120 |
| Option filter | Should this candidate option be available? | Boolean | @optioncode != 99 |
| Critical rule | Is the submission requirement satisfied? | Boolean | consent == 2 || IsAnswered(end_time) |
| Calculated variable | What value should be derived? | The variable's selected type | members.Count(person => person.age >= 18) |
An enabling condition cannot return a person's name or a count. A String calculated variable cannot return true. Designer's compiler uses types to catch many such mismatches.
2. What @optioncode means in an option filter
@optioncode is a special Survey Solutions context value available while Designer evaluates an option filter for an ordinary categorical question. It means: the numeric code of the candidate category being considered right now. It is not a questionnaire variable that you create, and it is not the respondent's answer.
true to keep that candidate or false to hide it.| Candidate category | Value of @optioncode | If the filter is list_fruits.Contains(@optioncode) |
|---|---|---|
| Mango | 1 | Show Mango only when code 1 was selected. |
| Oranges | 2 | Show Oranges only when code 2 was selected. |
| Pineapples | 3 | Show Pineapples only when code 3 was selected. |
| Kiwi | 4 | Show Kiwi only when code 4 was selected. |
If list_fruits contains codes 1 and 4, the filter is true while Designer tests Mango and Kiwi and false while it tests Oranges and Pineapples. The later question therefore displays only Mango and Kiwi.
Code, label and position are different
| Concept | Fruit example | What it means |
|---|---|---|
| Category code | 4 | The stored numeric value used by logic and exports. This is what @optioncode supplies. |
| Category title or label | Kiwi | The text shown to the interviewer. Translating or editing this title does not change @optioncode. |
| Display position | Fourth option | Where the option appears. Moving it does not turn its code into 4; only the configured value determines the code. |
Common patterns
@optioncode != 99
Every category except code 99 remains available.
list_fruits != null && list_fruits.Contains(@optioncode)
The null guard handles the time before the earlier multi-select has an answer.
Do not confuse the context symbols
| Symbol | Meaning | Typical location |
|---|---|---|
@optioncode | The candidate category code currently being tested. | Option filter for an ordinary user-defined or reusable categorical list. |
self | The answer currently being validated. | The question's validation condition. |
@rowcode | The identity/code of the current roster row. | Logic evaluated inside a roster row. |
A variable such as list_fruits | An actual answer stored in the interview. | Expressions wherever scope permits the reference. |
- Write
@optioncodeexactly as shown. Do not put it in quotes and do not use SurveyCTO's${...}notation. - The option filter controls which categories are offered; it does not enable or disable the whole question. Add a separate enabling condition when needed.
- When one question filters another, their codes must carry the same meaning. Reusable categories are the safest way to keep the codes aligned.
- A linked categorical question built from a List question or roster has a different candidate context. Do not assume every linked-source filter can use an ordinary
@optioncoderecipe. - After changing an earlier answer, test what happens to a choice already selected in the filtered question on both Web Tester and Interviewer.
choice_filter and a Survey Solutions option filter evaluate candidate choices one at a time. SurveyCTO can filter using columns stored on each row of the choices sheet. In an ordinary Survey Solutions categorical filter, @optioncode exposes the candidate's numeric code; arbitrary XLSForm choice-row columns do not automatically exist and may require reusable-category structure, encoded codes, or a redesigned rule.The official syntax guide calls @optioncode a substitution variable for non-linked categorical questions. Filtered answer options β
3. Variable names replace ${...}
${resp_age} >= 18
${sex} = 2
selected(${assets}, '3')resp_age >= 18 sex == 2 assets.Contains(3)
resp_age,sexandassetsare variable names assigned in Designer.- Do not add
${...}. Braces belong to the XLSForm/XPath convention. - Answer codes such as
2and3are values, not row positions and not labels. - Text literals use double quotes, for example
district_name == "Pavia".
4. Know what kind of value you are handling
A method or operator only makes sense for particular types. You cannot use .Contains(3) on a numeric age, add two text labels as though they were numbers, or compare a multi-select array directly with one code.
| Survey element | Think of its value as | Common operations | Example |
|---|---|---|---|
| Numeric question | A nullable whole number or decimal | Comparison, arithmetic, HasValue | age.HasValue && age >= 18 |
| Single-select question | One nullable integer answer code | ==, !=, InList(...) | status.InList(1, 2, 4) |
| Ordinary multi-select | An array of selected integer codes | Contains, Length | assets.Contains(3) |
| Text question | A string | Length, StartsWith, Contains | caseid.StartsWith("HH-") |
| Date question | A nullable date/time value | Date comparison and date properties | visit_date.HasValue |
| Roster | A collection of rows | Any, All, Count, Where | members.Any(person => person.age < 5) |
The exact underlying types vary by question kind. In the official type tables, a question mark such as int? means the value may be unanswered. Survey Solutions data types β
5. Operators and grouping
| Meaning | Survey Solutions syntax | Example |
|---|---|---|
| Equals / does not equal | == / != | sex == 2 |
| Greater/less than | >, >=, <, <= | age >= 18 |
| AND | && | age >= 18 && sex == 2 |
| OR | || | status == 1 || status == 2 |
| NOT | ! | !assets.Contains(99) |
| Add/subtract/multiply/divide | +, -, *, / | amount + fee |
| Choose between two values | condition ? A : B | age >= 18 ? "Adult" : "Minor" |
consent == 1 && (region == 2 || region == 4). Without the parentheses, the expression may compile but implement a different survey rule.6. Missing is a state, not zero or empty text
Numeric, date and categorical questions can be unanswered. Survey Solutions documents them as nullable types. Decide what an unanswered value should mean in the particular rule instead of assuming it is zero.
IsAnswered(resp_age) resp_age.HasValue
IsAnswered(...) is often the clearest survey-language check. HasValue is useful for nullable values in calculations.
IsAnswered(resp_age) && resp_age >= 18
This states the intended rule explicitly: an unanswered age is not an eligible adult.
Do not translate SurveyCTO's ${income} != '' mechanically. For a numeric Survey Solutions question, use IsAnswered(income) or income.HasValue. A validation is evaluated only when the question has an answer, so IsAnswered(self) does not make an unanswered question mandatory. Use critical questions/rules or downstream enabling when requiredness matters.
7. self means the answer being validated
self inside a question's validation condition. If the age question must be between 0 and 120, write self >= 0 && self <= 120. This keeps the validation portable if the question is renamed or copied. self is not used to enable the same question because an element cannot decide whether to turn itself on from an answer it cannot yet receive.8. Scope: one row versus the whole roster
Inside the current members roster row, a bare reference such as age means the age in that current row. Outside the roster, there are many ages, so you must say how the whole collection should be evaluated.
age >= 18
Read: βIs this member an adult?β
members.Any(person =>
person.age >= 18)Read: βDoes at least one member have age 18 or above?β
9. What exactly is person?
person is a temporary row name chosen by the questionnaire author. It represents one row while Survey Solutions checks the rows in members. It is not a question, roster, reserved keyword, respondent, interviewer account, or server user.Break the expression into five pieces:
| Piece | Meaning in members.Count(person => person.age >= 18) |
|---|---|
members | The roster collection to inspect. |
Count | The action: return how many rows satisfy the test. |
person | A temporary name for the row currently being tested. |
=> | Read as βsuch thatβ or βfor each row, test whether.β |
person.age >= 18 | The Boolean test applied to each row. |
These are identical because the temporary name can be chosen freely:
members.Count(x => x.age >= 18) members.Count(member => member.age >= 18) members.Count(person => person.age >= 18)
You must use the chosen name consistently. members.Count(person => x.age >= 18) fails because the expression declares person but then refers to an undeclared x.
members is an existing household roster with one row per person and questions named name, age, sex and relationship. Here you are learning how to read a collection expression. Module 6 teaches the functions in depth; Module 7 shows how the roster and its rows are actually created.10. Choose the collection operation from the question you are asking
| Survey question | Operation | Result type | Example |
|---|---|---|---|
| Does at least one row qualify? | Any | Boolean | members.Any(p => p.age < 5) |
| Does every row qualify? | All | Boolean | members.All(p => IsAnswered(p.age)) |
| How many rows qualify? | Count | Long Integer | members.Count(p => p.sex == 2) |
| Keep only qualifying rows | Where | A filtered collection | members.Where(p => p.age >= 18) |
| Take one field from each row | Select | A value collection | members.Select(p => p.name) |
Official LINQ and lambda guide β
11. Translate in small steps
Rule: enable the remittance module if the respondent is at least 18 and selected Bank transfer.
- Identify types:
resp_ageis numeric;remit_channelsis multi-select; Bank transfer code is1. - Translate each test:
resp_age >= 18;remit_channels.Contains(1). - Join both required tests with
&&. - Confirm the final expression returns Boolean.
resp_age >= 18 && remit_channels.Contains(1)
Syntax drill
3 attempts Β· answer reveals after attempt 3Task: Write the Survey Solutions enabling-condition expression for a respondent who is at least 18 years old and selected Bank transfer.
| Meaning | Variable / code |
|---|---|
| Respondent age | resp_age |
| Channels selected (multi-select) | remit_channels |
| Bank transfer answer code | 1 |
Expression troubleshooting checklist
- Location: must the expression return Boolean, text, a number, or a date?
- Name: does every reference exactly match a Designer variable name?
- Type: is the value scalar, nullable, multi-select, text, or a roster collection?
- Scope: are you in one roster row or querying the full roster?
- Missingness: what should happen while a dependency is unanswered?
- Grouping: do parentheses preserve the intended AND/OR logic?
- Behavior: after it compiles, did you test true, false, unanswered, and changed-answer cases?
Compilation proves that Designer can interpret the expression. It does not prove that the expression implements the intended survey rule. Syntax overview β
consent_q_01 == 1 to the main section. Add one validation from the practice reference, compile, and test a Yes and No consent path. Calculated variables and reusable questionnaire logic
Module 5 taught how to read an expression. This module shows when to save an expression as a calculated variable, how to choose its type, how it reevaluates, and how to connect the result to questionnaire flow and quality checks.
calculate field maps most closely to a Survey Solutions calculated variable. In Survey Solutions, you separately define the variable name, output type, expression, label, and export behavior.1. Decide whether you need a calculated variable
- it is short;
- it is used once;
- its meaning is immediately clear.
consent == 1
- the result is reused;
- it needs a meaningful name;
- it will be exported or piped into text;
- the underlying expression is complex.
is_eligible
For example, instead of repeating members.Any(person => person.age < 18) in four sections, create Boolean variable has_child and reference that name.
2. Build the variable in five deliberate steps
- State the rule in ordinary language. Example: βCount household members aged 18 or older.β
- Choose the output type. A count is a Long Integer.
- Choose a meaningful name. Example:
adult_count. - Write only the expression. Do not put an XLSForm field declaration or assignment statement in the expression box.
- Test dependency changes. Add an adult, change the adult to age 17, remove the row, save and reopen.
type: calculate name: adult_count calculation: count-if(...)
Type: Long Integer
Name: adult_count
Expression:
members.Count(person =>
person.age >= 18)3. Choose the output type from the meaning
| Type | Use it for | Example expression |
|---|---|---|
| Boolean | Eligibility, flags, yes/no logical results | resp_age >= 18 && recent_remit == 1 |
| Long Integer | Counts, whole-number scores, completed age | members.Count(person => person.age >= 18) |
| Double | Amounts, rates, means, measurements with decimals | amount_sent + transfer_fee |
| String | Classifications, constructed text, display summaries | resp_age >= 18 ? "Adult" : "Minor" |
| Date/Time | Derived dates and timestamps | start_date.HasValue ? start_date.Value.AddDays(14) : start_date |
true feels like β1.β4. Start with scalar calculations
| Meaning | Variable |
|---|---|
| Transfer amount | amount_sent |
| Transfer fee | transfer_fee |
Double variable total_sender_cost:
amount_sent + transfer_fee
Because the questions are nullable, the result is not a meaningful total until its dependencies are answered. Do not silently turn missing into zero unless that is the questionnaire's defined rule. If missing really means zero for both components, state that explicitly:
(amount_sent ?? 0) + (transfer_fee ?? 0)
5. Build a Boolean flag, then use it
| Meaning | Variable / code |
|---|---|
| Respondent age | resp_age |
| Recent remittance | recent_remit; Yes = 1 |
Boolean variable is_eligible:
IsAnswered(resp_age) && resp_age >= 18 && recent_remit == 1
A later section can now use the readable enabling condition is_eligible. The variable centralizes the definition, but changing it also changes every dependent section, validation, filter or text substitution. Test all consumers after editing it.
6. Working household-roster model
The rest of this module uses one consistent example. Assume the questionnaire already contains a roster called members. Module 7 will show how a numeric, list, multi-select or fixed source creates its rows.
| Variable | Type and codes | Meaning |
|---|---|---|
member_name | Text | Name or roster title. |
age | Integer, nullable | Completed age in years. |
sex | Single-select: Male = 1, Female = 2 | Recorded sex category. |
relationship | Single-select: Head = 1, Spouse = 2, Child = 3 | Relationship to household head. |
school_attend | Single-select: Yes = 1, No = 2 | Current school attendance. |
monthly_income | Double, nullable | Monthly individual income. |
The sample household is Amina, age 42, female and head; Bilal, age 18, male and child; and Sana, age 11, female and child. Their monthly incomes are 20,000, 5,000 and unanswered respectively. Keeping this model fixed makes each function's effect easier to see.
7. Collection-function map
Read a collection expression from left to right: start with a collection β optionally filter rows β choose or aggregate values β return the required type.
| Function | Question it answers | Returns | Typical household use |
|---|---|---|---|
Contains | Does this array or text contain a value? | Boolean | Whether asset code 3 was selected. |
Any | Does at least one row satisfy the rule? | Boolean | Whether any child under five lives in the household. |
All | Does every row satisfy the rule? | Boolean | Whether every member's age is answered. |
Count | How many rows exist or qualify? | Whole number | Number of members, children or eligible women. |
Where | Which rows qualify? | Filtered collection | Keep members aged 5β17 before another operation. |
Select | Which field should be taken from each row? | Value collection | Take names from eligible member rows. |
Sum | What is the total? | Number | Total household income or education spending. |
Min / Max | What is the smallest/largest answered value? | Number | Youngest/oldest age or earliest/latest event value. |
FirstOrDefault | What is the first match, if one exists? | One row/value or a default | Retrieve a known unique head value after validating uniqueness. |
String.Join | How can values be combined into display text? | String | Show the names of eligible members in an instruction. |
Where and Select do not finish most calculations. They return collections. Follow them with an operation such as Count, Sum, Min, Max, FirstOrDefault or String.Join when a calculated variable needs one scalar result.8. Contains, InList and Length: selection and membership
Contains becomes handy whenever one answer can contain several values. The object before the dot determines what βcontainsβ means.
assets.Contains(3)
Returns true when asset code 3 is selected. Use assets.Length to count selected codes.
caseid.Contains("HH-")Checks whether the text contains that character sequence. For a required prefix, caseid.StartsWith("HH-") is more precise.
A single-select has only one code, so compare it directly or use InList for several accepted codes:
relationship == 1 relationship.InList(1, 2)
The second expression means βhead or spouse.β Do not use Contains on a normal single-select answer.
9. Any: does at least one member qualify?
members.Any(person =>
person.age < 5)
This returns Boolean. It is useful for enabling a child-health section, setting an eligibility flag, or warning that an age-specific module is expected. In the sample household it returns false.
Any() without a condition asks whether the roster has at least one row:
members.Any()
Use Any when you need yes/no existence. Do not calculate Count(...) > 0 unless you also need the count.
10. All: does every existing row qualify?
members.All(person =>
IsAnswered(person.age))
This is useful for a completeness flag before enabling a household summary. Be aware of the empty-collection rule: All(...) is true when there are no rows because no row violates the condition. If the household must contain at least one member, write both requirements:
members.Any() &&
members.All(person =>
IsAnswered(person.age))
11. Count: total rows, qualifying rows and answered rows
| Expression | Meaning | Sample result |
|---|---|---|
members.Count() | All existing roster rows. | 3 |
members.Count(p => p.age < 18) | Members classified as children. | 1 |
members.Count(p => p.age >= 18 && p.sex == 2) | Adult women. | 1 |
members.Count(p => IsAnswered(p.monthly_income)) | Members with income answered. | 2 |
For an ordinary multi-select question, use assets.Length rather than treating it like a roster. For a yes/no-mode multi-select, assets.Yes.Length, assets.No.Length and assets.Missing.Length count each response state.
12. Where: keep qualifying rows for the next operation
members.Where(person =>
person.age >= 5 &&
person.age <= 17)
Where produces a filtered sequence of school-age member rows. By itself it is not a number or Boolean. Chain another function according to the question:
// How many school-age members? members .Where(p => p.age >= 5 && p.age <= 17) .Count() // Do any school-age members not attend school? members .Where(p => p.age >= 5 && p.age <= 17) .Any(p => p.school_attend == 2)
You can often express the second rule more simply as one Any predicate. Use Where when the filtered collection feeds a later projection or aggregation and improves readability.
13. Sum: household totals and conditional totals
Double variable household_income:
members.Sum(person =>
person.monthly_income)
Survey Solutions' migration guidance states that unanswered numeric values are ignored by Sum. In the sample household the result is 25,000. If βnobody answeredβ must remain different from a genuine total of zero, guard that state explicitly:
members.Any(p => IsAnswered(p.monthly_income)) ? members.Sum(p => p.monthly_income) : (double?)null
A conditional household total filters first:
members
.Where(p =>
p.age >= 18 &&
IsAnswered(p.monthly_income))
.Sum(p => p.monthly_income)
Sum can ignore it. Decide whether the indicator is valid when some members have missing income. You may need a completeness rule alongside the total.14. Min and Max: youngest, oldest and range checks
Long Integer variables for youngest and oldest answered age:
members.Any(p => IsAnswered(p.age)) ? members.Min(p => p.age) : (long?)null
members.Any(p => IsAnswered(p.age)) ? members.Max(p => p.age) : (long?)null
Unanswered values are ignored by these aggregates, but an empty set has no meaningful minimum or maximum. The Any guard makes that state explicit. These functions are useful for household eligibility, age-range QA, finding the latest event number, or checking the largest plot size.
15. Select: project one field from each qualifying row
Select changes a collection of member rows into a collection of values:
members.Select(person =>
person.member_name)
This does not yet produce a String variable. It produces a sequence of names that another operation can consume. A conditional projection combines Where and Select:
members .Where(p => p.age >= 18) .Select(p => p.member_name)
Use Select when the next step needs values rather than entire roster rows.
16. String.Join: turn projected values into readable text
String variable adult_names:
String.Join(", ",
members
.Where(p =>
p.age >= 18 &&
IsAnswered(p.member_name))
.Select(p => p.member_name))
In the sample household the result is Amina, Bilal. It can be piped into an instruction such as βThe adults listed are %adult_names%.β This joins stored answer values; it is not a general way to retrieve localized categorical answer labels.
17. FirstOrDefault: retrieve one match carefully
If the design guarantees one household head, this expression obtains the head's age as a nullable value:
members .Where(p => p.relationship == 1) .Select(p => p.age) .FirstOrDefault()
FirstOrDefault is order-dependent and returns a default when no match exists. It can conceal zero matches or multiple heads, so pair it with a separate validation:
members.Count(p =>
p.relationship == 1) == 1
18. Read and build a chain in a fixed order
String.Join(", ",
members
.Where(p =>
p.sex == 2 &&
p.age >= 15 &&
p.age <= 49)
.Select(p => p.member_name))
members: start with all household-member rows.Where: retain women aged 15β49.Select: take the name from each retained row.String.Join: combine those names into one display string.
Change the final operation to change the question being answered: use Count() for how many, Any() for whether at least one exists, Sum(...) for a total, or String.Join(...) for a display list.
19. Function selection practice
| Survey requirement | Best starting pattern |
|---|---|
| Enable immunization if any child is under five. | members.Any(p => p.age < 5) |
| Validate exactly one household head. | members.Count(p => p.relationship == 1) == 1 |
| Calculate total income reported by adult women. | Where(...).Sum(...) |
| Display the names of children currently attending school. | String.Join(... Where(...).Select(...)) |
| Find the oldest answered age. | Any(answered) ? Max(...) : null |
| Check whether crop code 4 was selected. | crops.Contains(4) |
20. Construct scalar text and classifications
resp_age >= 18 ? "Adult" : "Minor"
Configured as a String variable.
first_name + " " + last_name
Useful when the two names are outside a roster or in the same current row.
Insert a calculated value into a question or static text with Survey Solutions text substitution, for example %adult_count%. Calculated results are otherwise not interviewer-editable answers.
21. Understand reevaluation and failure behavior
- Calculated variables reevaluate automatically when a referenced dependency changes.
- Interviewers cannot override the calculated result.
- In logical expressions, an exception is treated as false; in calculations, an exception produces null.
- A variable used only as a helper can be marked Do not export; an analytic indicator can be retained with a clear variable label.
- Cycles are invalid: variable A cannot depend on B while B also depends on A.
22. Connect calculations to questionnaire behavior
is_eligible
Controls whether a question, subsection, section or roster is active.
self <= total_income
Checks whether an entered answer is acceptable. It does not make an unanswered question mandatory.
consent == 2 || IsAnswered(end_time)
Supports submission policy when Headquarters configures critical-rule handling.
23. Test the dependency graph, not just the formula once
- Open the interview with all dependencies unanswered.
- Enter values that make the result true or non-zero.
- Change one dependency so the result becomes false, zero or a different classification.
- Clear a dependency and observe the intended missing-value behavior.
- For rosters, add, edit and delete a row.
- Save, reopen and synchronize where applicable.
- Confirm every dependent section, validation, filter and substituted text updates correctly.
24. SurveyCTO translation reference
| SurveyCTO idea | Survey Solutions pattern |
|---|---|
if(condition, A, B) | condition ? A : B |
count-if(...) | roster.Count(item => condition) |
| Does any row qualify? | roster.Any(item => condition) |
| Do all rows qualify? | roster.All(item => condition) |
| Conditional sum | roster.Where(item => condition).Sum(item => item.amount) |
join(...) | String.Join(", ", roster.Select(item => item.name)) |
join-if(...) | String.Join(", ", roster.Where(item => condition).Select(item => item.name)) |
Expression evaluation β Β· LINQ in rosters β
Read familiar SurveyCTO operations as Survey Solutions design decisions
Examples below are traced to the supplied ICM Integrated Survey workbook. Source excerpts are exact cell contents, not endorsements of their logic. Target examples are teaching designs that require Designer compilation and Android/web testing; no full instrument conversion or runtime equivalence is claimed.
Conditional flags, typed results, and special response codes
SurveyCTO source cells and expressions
survey!V634 Β· r_age_calc (calculate)
if(${r_age_unit}=3, ${r_age}, 0)
survey!V635 Β· r_age_calc_month (calculate)
if(${r_age_unit}=2, ${r_age},
if(${r_age_unit}=1, 0,'null'))
survey!V636 Β· r_age_calc_days (calculate)
if(${r_age_unit}=3, 'null',
if(${r_age_unit}=2, ${r_age}*30,
if(${r_age_unit}=1, ${r_age},'null')))The age helpers combine numbers with the literal string βnullβ. Quoted null is text, not an unanswered numeric value. Decide whether an age in days or months should count as zero completed years, and keep that decision separate from an unknown age.
Proposed Long integer r_age_calc, with Numeric Integer source questions: (r_age_unit == 3 && r_age.HasValue) ? (long?)r_age.Value : ((r_age_unit == 1 || r_age_unit == 2) && r_age.HasValue) ? (long?)0 : null
Conditional counts and sums express different intentions
SurveyCTO source cells and expressions
survey!V649 Β· spouse_count (calculate)
count-if(${new_hhmem},${r_relation}=2)
survey!V650 Β· head_count (calculate)
count-if(${new_hhmem},${r_relation}=1)
survey!V1230 Β· no_unearned_income (calculate)
sum-if(${inc_any_unearn_ltm}, ${inc_any_unearn_ltm} = 1)The form counts heads and spouses, and sums a yes/no income flag only when it equals 1. Summing a flag is a count only because its included value is exactly 1. Identify the roster owning the field before writing the aggregate.
Outside new_hhmem, Long integer: new_hhmem.Count(p => p.r_relation == 1) Validation of the intended single-head rule: new_hhmem.Count(p => p.r_relation == 1) == 1
Money, month counts, and sentinel precedence
SurveyCTO source cells and expressions
survey!V2187 Β· temp_icm_sale (calculate)
if(${icm_op} >= 0 and ${icm_no_sale} >= 0, ${icm_op} - ${icm_no_sale},
if(${icm_op} = -999 or ${icm_no_sale} = -999, -999,
if(${icm_op} = -888 or ${icm_no_sale} = -888, -888, '')))
survey!V2190 Β· temp_icm_sale_high (calculate)
if(${icm_op} >= 0 and ${icm_prof_avg_n} >= 0, ${icm_op} - ${icm_no_sale} - ${icm_prof_avg_n},
if(${icm_op} = -999 or ${icm_no_sale} = -999 or ${icm_prof_avg_n} = -999, -999,
if(${icm_op} = -888 or ${icm_no_sale} = -888 or ${icm_prof_avg_n} = -888, -888, '')))
survey!V2195 Β· add_months_to_twelve (calculate)
if(${icm_no_sale} != null and ${icm_prof_avg_n} != null and ${icm_prof_high_n} != null and ${icm_prof_low_n} != null,
if(${icm_no_sale} = -999 or ${icm_prof_avg_n} = -999 or ${icm_prof_high_n} = -999 or ${icm_prof_low_n} = -999, -999,
if(${icm_no_sale} = -888 or ${icm_prof_avg_n} = -888 or ${icm_prof_high_n} = -888 or ${icm_prof_low_n} = -888, -888,
${icm_no_sale} + ${icm_prof_avg_n} + ${icm_prof_high_n} + ${icm_prof_low_n})),
0)Nested if expressions propagate -999 and -888 and otherwise subtract month counts. These are stored answers, not null. Preserve which special code takes precedence. The row 2190 first branch subtracts icm_no_sale without testing it in that branch; review this dependency rather than silently copying it. Row 2195 uses an unquoted null token whose source meaning should be verified.
Proposed Double temp_icm_sale (same sentinel priority): (icm_op >= 0 && icm_no_sale >= 0) ? (double?)(icm_op - icm_no_sale) : (icm_op == -999 || icm_no_sale == -999) ? (double?)-999 : (icm_op == -888 || icm_no_sale == -888) ? (double?)-888 : null
Capping follow-up work with min and max
SurveyCTO source cells and expressions
survey!V863 Β· count_confirmed_currenthh (calculate)
sum(${ph1_confirmed_final})
survey!V864 Β· need_new_phones (calculate)
max(0, 3 - ${count_confirmed_currenthh})
survey!V865 Β· repeat_count_new_phn (calculate)
min(${need_new_phones}, ${temp_r_phone_calc})The phone workflow counts confirmations, computes the remaining slots to reach three, then caps new follow-ups by eligible candidates. Learn the dependency chain before translating its arithmetic. Math.Max and Math.Min require deliberate handling of nullable calculated values.
Long integer confirmed_count: repeat_hh_phone.Count(p => p.ph1_confirmed_final == 1) Long integer slots_needed: confirmed_count.HasValue ? (long?)Math.Max(0L, 3L - confirmed_count.Value) : null
Dates: calendar months are not elapsed days
SurveyCTO source cells and expressions
survey!V1602 Β· start_month (calculate)
int(substr(indexed-repeat(${nonagown_start}, ${repeat_nonagown2}, ${index_nonagownb3}), 0, 2))
survey!V1603 Β· start_year (calculate)
int(substr(indexed-repeat(${nonagown_start}, ${repeat_nonagown2}, ${index_nonagownb3}), 3, 7))
survey!V1605 Β· nonagown_mth_grant (calculate)
((int(format-date(today(), '%Y')) - 2025) * 12) + (int(format-date(today(), '%m')) - 6)
survey!V1606 Β· nonagown_mth (calculate)
((int(format-date(today(), '%Y')) - int(substr(indexed-repeat(${nonagown_start}, ${repeat_nonagown2}, ${index_nonagownb3}), 3, 7))) * 12) + (int(format-date(today(), '%m')) - int(substr(indexed-repeat(${nonagown_start}, ${repeat_nonagown2}, ${index_nonagownb3}), 0, 2)))
survey!V1607 Β· nonagown_possible_op (calculate)
min(${nonagown_mth}, ${nonagown_mth_grant})The business logic extracts month and year from a month-year string, then calculates calendar-month distance. SurveyCTO substr uses start/end positions; C# Substring uses start/length. Prefer validated date or year/month inputs to repeated string parsing. Choose a fixed interview reference date explicitly instead of assuming a continuously changing clock is equivalent.
Proposed Long integer months_apart, with Date/Time questions start_date and reference_date: (start_date.HasValue && reference_date.HasValue) ? (long?)((reference_date.Value.Year - start_date.Value.Year) * 12 + reference_date.Value.Month - start_date.Value.Month) : null
Section timers are event-dependent calculations
SurveyCTO source cells and expressions
survey!V611 Β· start_res_member_info (calculate_here)
once(duration())
survey!V2321 Β· end_icm_qx (calculate)
once(duration())
survey!V2322 Β· dure_icm_qx (calculate)
( ${end_icm_qx}-${start_icm_qx} ) div 60ICM captures once(duration()) at section boundaries and divides a difference by 60. The calculate_here type matters: it evaluates when that place is reached. A normal Survey Solutions variable recalculates and is not a drop-in first-visit timer.
Design decision, not a C# substitution: Use paradata for a documented section-duration measure, or explicit timestamp questions for a clearly defined operational event. For a verified seconds difference, divide by 60.0 to retain fractional minutes.
Reference guides: SurveyCTO expressions; calculation timing; Survey Solutions rosters; numeric lookup tables.
suffix_text, and test both a filled and blank suffix. The full roster-name expression is developed in Module 14. Rosters: SurveyCTO repeats, but with a source that defines the rows
Modules 5 and 6 treated members as an existing collection so you could learn how to query it. This module now explains where those rows come from, how their identities behave, and what happens when the source changes.
begin repeat β Roster. But instead of writing a repeat_count expression in a column, Survey Solutions asks you to choose what creates the roster rows.Any, Count, Where, Sum, Min and Max inspect the rows that already exist; they do not create or preserve roster rows.The four roster patterns
| Roster type | What creates rows? | Typical use | SurveyCTO intuition |
|---|---|---|---|
| Numeric roster | A numeric question | βHow many household members?β | repeat_count = ${hhsize} |
| List roster | Items typed into a list question | Names of people, plots, firms | Dynamic repeat from a list |
| Multi-select roster | Selected answer options | Owned assets, cultivated crops | Repeat over selected choices |
| Fixed roster | Items defined in Designer | 7 days, expenditure categories | Fixed repeat structure |
Example 1 β dynamic repeat count from a numeric question
integer hhsize
begin repeat members
repeat_count = ${hhsize}
text name
integer age
select_one sex sex
end repeat
Numeric question: hhsize Roster: members Source type: Numeric Source question: hhsize Inside roster: name age sex
If hhsize = 3, Survey Solutions creates three members rows. This is the closest equivalent of a SurveyCTO dynamic repeat_count. The official roster documentation defines numeric rosters exactly this way.
Example 2 β SurveyCTO count-selected() intuition
Suppose respondents select every crop they cultivate:
| Variable | Type | Codes |
|---|---|---|
crops | Multi-select | 1 = Maize, 2 = Rice, 3 = Groundnut, 4 = Cassava |
count-selected(${crops})
You may use the number selected to determine how many repeat instances you need.
You normally do not need to calculate the count first. Make crops the roster source directly. Each selected crop becomes one roster row.
Roster: crop_details Source type: Multi-select Source question: crops
crops.Length is the Survey Solutions equivalent of SurveyCTO count-selected(${crops}).Example 3 β yes/no multi-select roster
A multi-select question in yes/no mode can also trigger a roster. Only items marked Yes create roster rows. If you need the number marked Yes in an expression, use:
assets.Yes.Length
For example, if the asset list contains Radio, TV, Refrigerator and Motorcycle, and Radio + Motorcycle are marked Yes, the asset-detail roster has two rows.
Example 4 β list question as the roster source
Instead of asking βHow many members?β and then collecting names inside the roster, you can first collect a list:
This pattern is often more intuitive for household listings because the person's name can serve as the visible roster title.
Scaling a roster down: what happens if the source gets smaller?
This is important if you are used to changing repeat_count in SurveyCTO. The roster follows its trigger question. If a numeric trigger drops from 4 to 2, the roster drops from four rows to two. If a selected crop is unselected, that crop's roster row disappears. If an item is deleted from a list source, its corresponding roster row disappears.
hhsize = 4hhsize = 2index() equivalent: use @rowindex, but learn @rowcode too
SurveyCTO/ODK's repeat-position idea maps most closely to Survey Solutions @rowindex. The official migration guide maps position(xpath) to @rowindex.
@rowindex
The current position of the roster item inside its parent roster.
It is zero-based:
first row β @rowindex = 0 second row β @rowindex = 1 third row β @rowindex = 2
If rows are deleted, the remaining rows can be renumbered.
@rowcode
The identity/code of the current roster item.
Its meaning depends on the roster source:
- numeric roster: generated row code;
- multi-select roster: selected option code;
- fixed roster: item code defined in Designer;
- list roster: stable item code, which may have gaps after deletions.
@rowcode as another spelling of index(). For position, think @rowindex. For identity, think @rowcode.Pulling a name out of a roster: SurveyCTO indexed-repeat()
Your SurveyCTO pattern creates household-level calculations from individual repeat instances. For example, r_comp_name is the name field inside repeat new_hhmem:
new_calc_hh_mem_name1 = indexed-repeat(${r_comp_name}, ${new_hhmem}, 1)
new_calc_hh_mem_name2 = indexed-repeat(${r_comp_name}, ${new_hhmem}, 2)
new_calc_hh_mem_name7 = indexed-repeat(${r_comp_name}, ${new_hhmem}, 7)
The three arguments mean field to retrieve, containing repeat, one-based instance number. These expressions request the first, second, and seventh names. SurveyCTO also supports additional repeat/index pairs for nested repeats. Its documentation notes fallback to instance 1 for an invalid index, so do not assume an invalid request always returns blank. Test the source behavior before reproducing it. SurveyCTO indexed-repeat reference β
A positional retrieval pattern in Survey Solutions
For this exercise, create a roster named new_hhmem containing Text question r_comp_name. Inside that roster add a Long integer calculated variable named member_position, with expression @rowindex + 1. It exposes the current row's position as a one-based number for this comparison. Outside the roster, add these String calculated variables:
// new_calc_hh_mem_name1 new_hhmem.Where(p => p.member_position == 1) .Select(p => p.r_comp_name).FirstOrDefault() ?? "" // new_calc_hh_mem_name2 new_hhmem.Where(p => p.member_position == 2) .Select(p => p.r_comp_name).FirstOrDefault() ?? "" // new_calc_hh_mem_name7 new_hhmem.Where(p => p.member_position == 7) .Select(p => p.r_comp_name).FirstOrDefault() ?? ""
Enter each expression without its explanatory comment. Where finds the requested position; Select extracts its name; FirstOrDefault retrieves one value or null; ?? "" displays an empty string for null. The alias p means one roster row. Place static text outside the roster containing Second member: %new_calc_hh_mem_name2% to see the result.
new_hhmem.Any(p => p.member_position == 7) to check row existence separately.Do not mechanically translate the seventh instance into new_hhmem[7]. Roster addressing must respect row codes and the roster source; a position and a person's identity are different. The explicit position variable makes the intended rule visible. Compile these expressions in your Designer version and test the row changes below before using them in a project.
Choose the retrieval rule that matches the survey question
| What you mean | Pattern |
|---|---|
| The current member's name, inside the same roster | Refer directly to r_comp_name; use %r_comp_name% in question text. No cross-roster retrieval is needed. |
| The second member in the current ordering | Use the positional calculation above. Deleting an earlier row can change who is second. |
| The household head | Filter by the relationship code, for example new_hhmem.Where(p => p.relationship == 1).Select(p => p.r_comp_name).FirstOrDefault() ?? "", only if 1 means head. Validate that exactly one row qualifies; the first row is not necessarily the head. |
| A respondent-selected member | Use a linked question and its row identity (Module 8), rather than storing the member's current position. |
| All entered names for a summary | String.Join(", ", new_hhmem.Where(p => !String.IsNullOrEmpty(p.r_comp_name)).Select(p => p.r_comp_name)). This produces display text, not a member identifier. |
In your screenshot, count(${new_hhmem}) counts repeat instances. The corresponding roster count is new_hhmem.Count(); counting answered names instead requires a predicate. Do not use a count of distinct name strings as a count of unique people: two members may share a name, and spelling differences can make one name appear distinct.
survey!W970 contains the spreadsheet formula ="${"&B969&"}"; its cached result is ${fd_cons_prep_cnt}. The first builds text in Excel; the second instructs SurveyCTO how many repeat instances to create. In Designer, reconstruct the intended roster source rather than pasting an Excel formula. Cached workbook values can be stale, so verify the effective source definition.Why the distinction matters after deleting list items
Using row identity in conditions
Suppose a fixed roster has codes 10 = Matches and 11 = Cigarettes. Inside that roster you can apply different rules by row:
@rowcode == 10 ? self <= 20 : @rowcode == 11 ? self <= 100 : true
The system-generated @rowcode exists specifically for referring to particular roster rows.
Dynamic roster titles
To make roster questions intuitive for interviewers, use the roster's title/name in question text. For a member roster named members, you can write wording such as:
How old is %rostertitle%?
If the current roster title is Amina, the interviewer sees How old is Amina? The roster identifier form, such as %members%, is also supported, but %rostertitle% makes the intent explicit while working inside the current roster. Neither form is valid outside its applicable roster scope.
Nested rosters
You can put one roster inside anotherβfor example:
Household
ββ members
ββ name
ββ age
ββ jobs
ββ employer
ββ earnings
Survey Solutions supports nested rosters, but current limits cap nesting depth and total roster instances. Complex nested designs should be tested with realistic maximum sizes rather than only with tiny test households.
Table presentation for web interviews
For CAWI/web interviewing, some simple rosters can be displayed as a table, with roster items as rows and questions as columns. This can be useful for compact modules such as prices or simple household grids. It is subject to design restrictions and falls back to the normal subsection-style roster on tablets.
| Member | Age | Years schooling |
|---|---|---|
| Amina | 42 | 12 |
| Bilal | 18 | 13 |
| Sana | 11 | 5 |
Practical design rules
Choose the trigger deliberately.
Numeric when only quantity matters; list when names/items are entered; multi-select when rows come from known categories.
Set sensible maximums.
Multi-select and list trigger questions should have maximum sizes. Numeric triggers should be constrained with validation such as self <= 30.
Know identity vs position.
Use @rowindex for position and @rowcode for stable row identity/code.
- Household members whose names must be visible later.
- Remittance channels selected from a fixed list.
- Exactly seven days of the week.
- A respondent reports they made
ntransfers and you need one row per transfer.
Show suggested answers
1. List roster, because names themselves are useful as roster titles and later linked choices. 2. Multi-select roster. 3. Fixed roster. 4. Numeric roster triggered by the number of transfersβunless you first collect meaningful transfer labels in a list, in which case a list roster may be more intuitive.
From roster source to household indicator
This final sequence connects the three modules without mixing their responsibilities:
| Step | Design decision | Example | Taught in |
|---|---|---|---|
| 1 | Create stable person rows. | List question member_names triggers roster members. | Module 7 |
| 2 | Collect typed answers inside each row. | age, sex, relationship, monthly_income. | Modules 5 and 7 |
| 3 | State the household-level question. | βHow many adult women are listed?β | Module 5 |
| 4 | Choose the function by required result. | A number is required, so use Count. | Module 6 |
| 5 | Write and name the calculation. | members.Count(p => p.age >= 18 && p.sex == 2) | Module 6 |
| 6 | Test row lifecycle transitions. | Add, edit and delete a member; confirm the count and dependent logic update. | Modules 6 and 7 |
Official documentation for this module
- Rosters: numeric, list, multi-select and fixed types
- Migration guide: repeat groups,
position()β@rowindex,count-selected()β.Length - System-generated variables including
@rowcode - Tabular roster presentation for web interviews
- Current Survey Solutions roster limits
- Protecting preloaded trigger values from reduction
- Survey Solutions community explanation of automatic answer removal when roster rows are removed
Read familiar SurveyCTO operations as Survey Solutions design decisions
Examples below are traced to the supplied ICM Integrated Survey workbook. Source excerpts are exact cell contents, not endorsements of their logic. Target examples are teaching designs that require Designer compilation and Android/web testing; no full instrument conversion or runtime equivalence is claimed.
Parallel repeats: join people by identity, not only position
SurveyCTO source cells and expressions
survey!W613 Β· repeat_res_member_info (begin repeat)
${hhmem_new_num_push}
survey!V614 Β· index_res_member_info (calculate)
index()
survey!V615 Β· index_res_kids_id (calculate)
${index_res_member_info} + 18
survey!V616 Β· temp_res_member_info_name (calculate)
indexed-repeat(${r_fname}, ${new_hhmem}, ${index_res_member_info})
survey!V619 Β· temp_res_member_info_rela (calculate)
indexed-repeat(${r_relation}, ${new_hhmem}, ${index_res_member_info})The demographics repeat has the same count as new_hhmem and pulls names and relationships with indexed-repeat. The +18 offset reserves codes after baseline members. This is an instrument-specific ID scheme, not a Survey Solutions roster rule.
Proposed design: One members roster holds name, stable source_member_id, relationship, residency status, age and education subsections. Use a separate source_member_id field to preserve old identifiers. Do not assign @rowcode = index + 18 or equate @rowindex with a persistent ID.
Filtered names, ages, and IDs: keep rows together
SurveyCTO source cells and expressions
survey!V746 Β· join_hh_mem_older_six2 (calculate)
join-if(',', ${info_current_all_index}, ${age_hhmem} >= 6)
survey!V748 Β· count_hh_mem_older_six (calculate)
count-if(${info_current_all}, ${age_hhmem} >= 6)
survey!V749 Β· join_hh_mem_older_thirteen2 (calculate)
join-if(',', ${info_current_all_index}, ${age_hhmem} >= 13)
survey!V750 Β· join_hh_mem_13_older (calculate)
join-if(',', ${age_hhmem}, ${age_hhmem} >= 13)
survey!V751 Β· join_hh_mem_older_13 (calculate)
join-if(',', ${info_current_all_name}, ${age_hhmem} >= 13)
survey!V752 Β· count_hh_mem_older_13 (calculate)
count-if(${info_current_all}, ${age_hhmem} >= 13)
survey!W1038 Β· repeat_earned_inc (begin repeat)
${count_hh_mem_older_13}
survey!V1040 Β· any_work_screen_name (calculate)
item-at(',', ${join_hh_mem_older_13}, (${index_any_work_screen} - 1))
survey!V1041 Β· any_work_screen_memid (calculate)
item-at(',', ${join_hh_mem_older_thirteen2}, (${index_any_work_screen} - 1))
survey!V1042 Β· any_work_screen_age (calculate)
item-at(',', ${join_hh_mem_13_older}, (${index_any_work_screen} - 1))ICM constructs parallel comma-separated lists and then reads the same position from each. A missing value, delimiter in a name, or inconsistent filter can break alignment. In Survey Solutions, keep values in the member row and enable an age-eligible subsection, or use a linked selection for eligible people.
Assuming age_hhmem is numeric inside info_current_all:
Household-level Long integer:
info_current_all.Count(p => p.age_hhmem >= 13)
Household-level String display summary:
String.Join(", ", info_current_all.Where(p => p.age_hhmem >= 13)
.Select(p => p.info_current_all_name))
Within that roster, income subsection enabling:
age_hhmem >= 13Selected-item repeats become categorical rosters
SurveyCTO source cells and expressions
survey!W884 Β· repeat_fd_cat (begin repeat)
9.0
survey!V891 Β· fd_cons_cnt (calculate)
count-selected(${fd_cons_1})
survey!W892 Β· repeat_fd_cons (begin repeat)
${fd_cons_cnt}
survey!V894 Β· fd_cons_val (calculate)
selected-at(${fd_cons_1},index()-1)
survey!V895 Β· fd_cons (calculate)
pulldata('preload_fd_cat', 'fd_cons_name', 'cons',${fd_cons_val})The food section has nine outer categories, then one repeat per selected food item. selected-at retrieves a choice code by zero-based selection position. A fixed outer roster plus an ordinary multi-select-triggered inner roster can retain the hierarchy without a calculated repeat count and selection-position helper.
Proposed design: Fixed food-category roster β multi-select fd_cons_1 β item roster. Inside the item roster, @rowcode identifies the selected category code; use the chosen item labels for roster titles. Preserve multilingual labels through translations, not numeric lookup columns.
Nested child counts: validate both identity and age bands
SurveyCTO source cells and expressions
survey!W702 Β· r_kids (select_multiple old_new_hh_members)
survey!W703 Β· repeat_kids (begin repeat)
count-selected(${r_kids})
survey!V704 Β· kid_age (calculate)
selected-at(${join_age_kids_15}, index() - 1)
survey!V705 Β· kid_age3 (calculate)
if(${kid_age} <0 and ${kid_age} <=2, 1, 0)
survey!V706 Β· kid_age3to8 (calculate)
if(${kid_age} >2 and ${kid_age} <=8, 1, 0)
survey!V707 Β· kid_age9to15 (calculate)
if(${kid_age} >8 and ${kid_age} <=15, 1, 0)
survey!V709 Β· r_kids_baby (calculate)
sum(${kid_age3})A parent selects children, but kid_age reads the position in join_age_kids_15. Verify that this list contains exactly those selected children in matching order. Also, kid_age3 currently tests age < 0 AND age <= 2: it will not flag any nonnegative infant age. That is observed source logic, not an approved age-band definition.
Proposed rule after confirming ages and child identities: children.Count(c => c.age >= 0 && c.age <= 2) children.Count(c => c.age > 2 && c.age <= 8) children.Count(c => c.age > 8 && c.age <= 15) Here children is a proposed child roster and age is numeric; it is not a literal rename of the ICM repeat.
Deduplicating lists and combining old/new records
SurveyCTO source cells and expressions
survey!V312 Β· count_join_nonagwork_name (calculate)
count-items(" ", de-duplicate(" ", ${join_nonagwork_num}))
survey!V1052 Β· join_ag_index (calculate)
de-duplicate(' ', join(' ', ${ag_index}))
survey!V1053 Β· join_nonag_index (calculate)
de-duplicate(' ', join(' ', ${nonag_index}))
survey!W1595 Β· repeat_nonagown3 (begin repeat)
${count_nonagown_list}+${nonagown_n_calc}de-duplicate and count-items convert strings into sets; the business count adds old and new records. Translate the underlying entities, not merely the delimiters. Deduplicate by a documented stable ID; equal labels do not establish equal people or businesses.
Proposed Long integer, assuming a redesigned members roster and String member_id: members.Where(p => !String.IsNullOrEmpty(p.member_id)) .Select(p => p.member_id).Distinct().Count()
Reference guides: SurveyCTO expressions; calculation timing; Survey Solutions rosters; numeric lookup tables.
member_fnames List question and use it to generate the members roster. Enter two synthetic names, then verify that the roster has exactly two rows. Linked questions: turning earlier answers into live answer options
This is one of Survey Solutions' most powerful features for complex household, labor, agriculture, education, health, and establishment surveys. A linked categorical question can take values collected earlierβoften people or roster rowsβand turn them into the answer options of a later question.
Simple example: choose the household head
You first collect household members:
membersmember_name inside roster membershh_headWho is the household head?
Survey Solutions' official categorical-question documentation describes linked questions exactly this way: their categories are generated during the interview from answers to a previous text, numeric, date question, or roster source. Categorical questions β
How you configure one in Designer
Linked describes the source of the answers. In the Designer interface shown by the trainee, use Source of categories; older documentation instead shows an Is linked checkbox.
| Source of categories | Meaning | Example |
|---|---|---|
| User defined categories | Options entered for this question. | Yes / No. |
| Reusable categories | A named category set in the questionnaire. | The synthetic grade_codes list you create. |
| List question or question from roster group | Choices obtained from the selected interview source. | Household members entered earlier. |
list_fruits is still a multi-select question, so it will not appear as a direct List-question source in this binding selector.If the binding dropdown is empty
- Confirm that the intended source is located before the linked question and has been saved.
- For a direct source, use an actual List question, or a supported Text, Numeric, or Date question located inside a roster.
- If the aim is to show a subset of the same fixed categories selected in an earlier categorical multi-select, leave the linked-source selector. Give both questions the same category codes and use an
@optioncodefilter, as shown below. - Create a roster from the multi-select only when the questionnaire needs one rowβor several follow-up questionsβfor every selected category.
Suppose list_fruits is a categorical multi-select with codes 1 Mango, 2 Oranges, 3 Pineapples, and 4 Kiwi. The next question asks, βOf the fruits you selected, which is your favorite?β This is one answer chosen from a filtered fixed list, so it does not require a roster.
- Create
favorite_fruitas a Categorical: Single-select question. - Use the same four category codes and titles. Prefer one Reusable categories set bound to both questions; separately entered categories also work only if their codes remain identical.
- Keep Source of categories as User defined categories or Reusable categories. Do not choose List question or question from roster group.
- Add this option filter:
list_fruits != null && list_fruits.Contains(@optioncode). If the symbol is unfamiliar, review Module 5: What@optioncodemeans. - Add this enabling condition so the question appears only after at least one fruit is selected:
list_fruits != null && list_fruits.Length > 0.
Designer evaluates the filter once for every candidate category. Here @optioncode is the code of the candidate currently being tested. If the respondent selected Mango (1) and Kiwi (4), only Mango and Kiwi appear in favorite_fruit.
Follow the behavior from Designer to the interview
The screenshots below show the two expressions doing different jobs. The enabling condition controls whether the whole favorite-fruit question is active. The option filter controls which fruit choices appear after the question becomes active.
1. Before a fruit is selected
list_fruits has no selected codes, so list_fruits != null && list_fruits.Length > 0 is false. The diagonal shading shows that the entire favorite-fruit question is disabled. The blue sentence is an interviewer instruction; it helps the interviewer but does not affect the logic.
2. After Mango and Pineapples are selected
The source multi-select now contains codes 1 and 3. The enabling condition is true, so the favorite-fruit question becomes active. The option filter tests every candidate and retains only Mango and Pineapples.
3. The matching Designer configuration
The screenshot uses variable name fav_fruits; the course example uses favorite_fruit. Either name is valid when used consistently. Notice that the Filter and Enabling condition are separate fields: filtering does not activate the question, and enabling does not reduce its options.
fruit_codesβbefore saving and compiling. Ideally bind both list_fruits and fav_fruits to that same set. Otherwise the filter can compile against codes whose labels or meanings do not match the source question.| Interview state | Enabling condition | Option filter result | What the interviewer sees |
|---|---|---|---|
| No fruits selected | false | Not operationally relevant while the question is disabled | The favorite question is shaded and unavailable. |
| Mango and Pineapples selected | true | True for codes 1 and 3; false for 2 and 4 | Only Mango and Pineapples appear as radio-button choices. |
| Selection later changes | Reevaluated | Reevaluated for every candidate code | The available favorite choices change; verify any previously selected favorite during testing. |
list_fruits and deselect Mango. Confirm how Web Tester handles the now-ineligible favorite. Repeat in the Interviewer application. Record whether the answer is cleared, flagged, or otherwise requires correction; this dependency-changing case matters more than testing only the happy path.| Questionnaire intention | Survey Solutions design |
|---|---|
| Choose one favorite from previously selected fixed fruits | Use the same categories plus list_fruits.Contains(@optioncode). No roster. |
| Ask quantity, price, frequency, or another set of questions for every selected fruit | Create a roster sourced by list_fruits and place the repeated questions inside it. |
| Let respondents type arbitrary fruit names, then choose one of those entered names | Use a true List question, then bind the later linked categorical question to that List. |
- Create a categorical single-select or categorical multi-select question.
- Open Source of categories and choose List question or question from roster group.
- In the binding/source selector that appears, choose the appropriate list question, supported question inside a roster, or roster where offered. The binding caption can vary by Designer version.
- If needed, add a Filter expression so only eligible source items appear. The roster filter examples below assume a question inside a roster; do not assume a direct list source exposes the same candidate fields.
- Save, Compile, and Test. Enter source names first, confirm the linked choices appear, then change or remove a source item and check the selection.
Single-select vs multi-select linked questions
Use when exactly one earlier item should be chosen.
- Who is the household head?
- Who is this person's spouse?
- Who owns the dwelling?
- Which plot is the largest?
Use when several earlier items may be chosen.
- Which members visited a doctor?
- Who received remittances?
- Which plots used fertilizer?
- Which household members are employed?
Filtering linked options: where this becomes powerful
The Filter field is evaluated once for every potential option. If the expression is true, that option is shown; if falseβor if evaluation throws an exceptionβthe option is hidden. Conceptually, it is an enabling condition applied separately to each possible answer option. Filtered options β
Example 1 β only adults can be household head
| Source roster | Variables |
|---|---|
members | member_name, age, sex |
Filter:
age >= 18
age >= 18Example 2 β women of reproductive age
If sex == 2 means female:
sex == 2 && age >= 15 && age <= 49
Survey Solutions' current syntax overview uses this exact kind of linked-roster filtering example. Filter syntax β
The important keyword: @current
This is where linked questions become especially useful inside a roster. When you are asking a question about the current roster row but filtering potential choices from the whole roster, you need a way to distinguish:
Variables written normallyβsuch as age, sex, or @rowcodeβrefer to the roster row currently being tested as a possible answer option.
Prefix with @current. to refer to the person/item whose roster row you are currently interviewing, e.g. @current.age.
Example 3 β choose the mother of each household member
Suppose the linked question mother sits inside the members roster. You want to show only candidates who are:
- female;
- not the current person;
- at least 10 years older than the current person.
sex == 2 && @rowcode != @current.@rowcode && age >= (@current.age + 10)
This is essentially the pattern used in Survey Solutions' official filtered-answer-options documentation. Filtered options β
Example 4 β spouse selection
Assume sex is coded 1 = male, 2 = female. A simplified opposite-sex spouse filter could be:
@rowcode != @current.@rowcode && sex != @current.sex && age >= 15
For a real survey you would normally add your survey's own age, marital-status, relationship, and plausibility rules rather than relying on sex alone.
Example 5 β double screening without creating another roster
Imagine you first ask a linked multi-select:
employed_members "Which household members are employed?"
Then you ask:
multiple_jobs "Which employed household members have more than one job?"
The second linked question can use the earlier dynamic list and filter it again. Survey Solutions explicitly added filtering of questions linked to a list to support this type of double screening. Linked-list filtering β
Example 6 β link to non-person rosters
Linked questions are not just for household members. Suppose you collect plots:
Plot A Β· 0.7 ha
Plot B Β· 2.1 ha
Plot C Β· 1.3 ha
Which plot received fertilizer?
You can link to the plot-name question or to meaningful roster titles, then filter by crop, size, ownership, irrigation status, or any other relevant variables.
Linking to text, numeric, date, and roster titles
| Source | What becomes the linked options? | Example |
|---|---|---|
| Text question in roster | Entered text values | Member names |
| Numeric question in roster | Entered numeric values | Plot IDs |
| Date question in roster | Entered dates | Episode dates |
| Roster itself | Roster row titles | Names from a list-triggered roster |
The categorical multi-select documentation explicitly lists text, numeric, date, and roster-linked sources. Categorical questions β
Linked question vs ordinary filtered categorical question
The options themselves are generated from earlier interview data.
Who is the head? [Amina] [Bilal] [Sana]
The option list is predefined in Designer; the filter merely hides some codes.
Payment method [Bank] [Cash] [Wallet]
In an ordinary categorical filter, use @optioncode to refer to the option currently being evaluated. Filtered options β
What gets stored? The label is not the data key
For example, if the roster contains:
row code 0 β Amina row code 1 β Bilal row code 2 β Sana
and a linked question selects Sana, the stored/exported answer is associated with code 2, while the interviewer sees the human-readable label Sana.
Multi-select linked questions can make wide exports
A linked multi-select may create many export columns because Survey Solutions has to allow for the potential maximum number of source records. This can surprise you if you come from SurveyCTO and expect a compact space-separated answer string. Survey Solutions' categorical multi-select documentation describes how linked multi-select exports depend on the source roster's potential size. Multi-select export β
Common mistakes
| Mistake | Why it happens | Better approach |
|---|---|---|
| Linking to the wrong source question | Several roster questions have similar titles | Pay attention to variable names in Designer's source selector. |
| Forgetting to exclude the current person | Filter looks only at sex/age | Add @rowcode != @current.@rowcode when self-selection is impossible. |
Using @current when the question is not in a roster context | Confusing candidate and current-row logic | Use @current only when you truly need the current roster occurrence. |
| Assuming hidden option = impossible forever | Filters are evaluated dynamically | Remember options can change when earlier answers change. |
| Using names as if they were unique IDs | What the interviewer sees feels like the stored value | Design around roster identity/codes; duplicated labels are possible. |
When I would reach for linked questions immediately
Mother, father, spouse, household head, respondent, caregiver, decision-maker.
Women 15β49, working-age adults, school-age children, members with disability, migrants.
Plots, crops, loans, firms, transfers, assets, health episodes, jobs, schools.
members with member_name, age, sex, relationship, and employed.
- Create
hh_head: one adult household member. - Create
working_members: select all employed adults. - Inside each member row, create
mother: another member who is female and at least 10 years older. - Inside each member row, create
spouse: another member, never the current member.
Show example filters
1. age >= 18
2. age >= 18 && employed == 1
3. sex == 2 && @rowcode != @current.@rowcode && age >= (@current.age + 10)
4. At minimum: @rowcode != @current.@rowcode, then add your survey's marital-status, age, sex, and relationship rules as appropriate.
Official documentation for this module
- Categorical multi-select questions: linked sources, filters, and exports
- Filtered answer options and
@current - System-generated variables including
@current - Overview of categorical question types, including linked questions
- Compile errors related to linked questions and filters
- Filtering questions linked to lists / double-screening example
- List-item codes and linked-question export behavior
Read familiar SurveyCTO operations as Survey Solutions design decisions
Examples below are traced to the supplied ICM Integrated Survey workbook. Source excerpts are exact cell contents, not endorsements of their logic. Target examples are teaching designs that require Designer compilation and Android/web testing; no full instrument conversion or runtime equivalence is claimed.
Selected codes, display labels, and linked member identities
SurveyCTO source cells and expressions
survey!V689 Β· info_current_all_index (calculate)
selected-at(${joined_old_new_hhmems}, index()- 1)
survey!V690 Β· info_current_all_name (calculate)
choice-label(${hh_resp}, ${info_current_all_index})
survey!V2229 Β· mem_old_rel_index (calculate)
if(selected(${icm_grp_mem_name_old}, 0), selected-at(${icm_grp_mem_name_old}, index()), selected-at(${icm_grp_mem_name_old}, index()-1))
survey!V2230 Β· mem_old_rel_lbl (calculate)
pulldata('icm_part_preloads', 'icm_part_og_name1', 'id', ${mem_old_rel_index})choice-label returns presentation text; selected-at returns a selected token. A linked Survey Solutions answer instead identifies a roster member. Preserve that identity for relationships; do not convert it into a label and later try to recover the member by name.
Design route: Keep respondent/member selection linked to the member roster. Use question text substitution or supported label-display behavior for wording. Retrieve a member attribute through the linked identity only after checking that the selection still exists; see the linked-question examples in this module.
Special options: count eligible choices explicitly
SurveyCTO source cells and expressions
survey!V2243 Β· cnt_icm_grp_mem_name_0 (calculate)
if(selected(${icm_grp_mem_name}, '0'), 0, count-selected(${icm_grp_mem_name}))
survey!V2244 Β· cnt_icm_grp_mem_name (calculate)
if(selected(${icm_grp_mem_name}, 0), (${cnt_icm_grp_mem_name_0}) - 1, count-selected(${icm_grp_mem_name}))
survey!W2258 Β· repeat_mem_oth_rel_ex (begin repeat)
${cnt_icm_grp_mem_name}
survey!V2259 Β· mem_rel_index_ex (calculate)
if(selected(${icm_grp_mem_name}, 0), selected-at(${icm_grp_mem_name}, index()), selected-at(${icm_grp_mem_name}, index()-1))When code 0 is selected, row 2243 yields 0 and row 2244 subtracts 1, yielding -1. This feeds a repeat count. Treat this as a source-review issue, not a pattern to reproduce. The selected-at offset also assumes a particular position for the special code.
Illustrative Long integer for an ordinary categorical multi-select: icm_grp_mem_name == null ? 0 : icm_grp_mem_name.Count(code => code != 0) Use this only after confirming 0 means a non-member option and preserving all other exclusions. This expression does not apply to a linked roster answer.
Reference guides: SurveyCTO expressions; calculation timing; Survey Solutions rosters; numeric lookup tables.
phone_owners as a linked multi-select sourced from members. In Tester, confirm the choices are the two names you entered, not a fixed reusable category list. Assignments, sample lists, and preloading household information
This is the Survey Solutions equivalent of one of the most common SurveyCTO case-management workflows: you already have a household sample with IDs, geography, household head, address, and sometimes earlier-wave information, and you want interviewers to receive the right case without asking all of that information again.
caseid and then pull information from the cases dataset. In Survey Solutions, the assignment itself is usually the case. Headquarters creates one assignment per sampled household and preloads the identifying information into it. The interviewer receives that household directly on their dashboard.SurveyCTO case management vs Survey Solutions
| SurveyCTO workflow | Survey Solutions equivalent |
|---|---|
| Cases dataset contains sampled households | Assignments represent the sample/workload |
| Enumerator selects a case | Enumerator receives assigned household cards after synchronization |
caseid identifies the case | Your household ID is typically an identifying question, while Survey Solutions also maintains its own internal interview/assignment IDs |
| Preload province, municipality, barangay, head, address | Upload those values into supported questions placed in Cover; their location makes them identifying automatically |
| Use case data to populate other fields | Use advanced preloading for interviewer/hidden questions and roster data |
| Case can be reassigned | Assignment can be reassigned to another supervisor/interviewer |
Concrete example: a household sample from the Philippines
Suppose your sampling team gives you this household list:
| hhid | province | municipality | barangay | hh_head | address | _responsible | _quantity |
|---|---|---|---|---|---|---|---|
| PH-001-0001 | Cavite | DasmariΓ±as | Paliparan III | Maria Santos | Blk 4 Lot 12 | int_ana | 1 |
| PH-001-0002 | Cavite | DasmariΓ±as | Paliparan III | Jose Reyes | Blk 5 Lot 8 | int_ana | 1 |
| PH-002-0001 | Laguna | Calamba | Real | Liza Cruz | Purok 2 | int_miguel | 1 |
For a fixed sample where each row is one household, this is usually the closest Survey Solutions equivalent of a SurveyCTO cases dataset. Each row becomes one assignment. The official documentation supports batch creation of assignments from a tab-delimited file, with optional _responsible and _quantity columns.
Step 1 β put identifying questions on the Cover page
In Designer, open the built-in Cover section and add the sample variables that should identify the household. Modern Designer has no Identifying checkbox: supported questions become identifying automatically when placed in Cover.
hhidprovincemunicipalitybarangayhh_headaddressIdentifying questions live on the Cover page. If Headquarters preloads their values, those values appear on the interviewer's assignment/dashboard and are locked: the interviewer can see them but cannot change them.
hh_head = "Maria Santos" as an identifying answer, the interviewer cannot simply edit that identifying answer to βMaria Dela Cruz.β If you want field staff to verify and correct sample information, use a deliberate confirmation pattern rather than expecting them to edit the locked identifying field.Step 2 β upload the sample as assignments
Place
hhid, geography, head name, address, etc. in Cover. Their placement makes them identifying.Survey Setup β Questionnaires β hover over and click the imported questionnaire row β Upload assignments.
The first row contains the identifying variable names expected by that questionnaire.
Add
_responsible if one file contains cases for multiple field staff; use _quantity = 1 for one interview per sampled household.HQ checks variable names, formats, and values before creating assignments.
The assigned households appear as cards on the Interviewer dashboard.
What the interviewer sees
This is why, for a household sample, the interviewer normally does not need to type or select a case ID and then run a lookup. The assignment already carries the household's identifying information.
Pattern A β sample information is authoritative: show it, do not edit it
Use this when the household ID and geography are fixed by the sampling frame and should never be changed by field staff.
Cover / Identifying: hhid = PH-001-0001 π province = Cavite π municipality = DasmariΓ±as π barangay = Paliparan III π sample_head = Maria Santos π sample_addr = Blk 4 Lot 12 π
The interviewer uses these values to find the correct household. No repeated questions are needed.
Pattern B β show sample information and ask the interviewer to confirm it
This is often the best pattern for your example. Keep the original sample values locked, then ask separate interviewer questions to verify that they have reached the intended household.
Head: Maria Santos
Barangay: Paliparan III
Address: Blk 4 Lot 12
For example:
Identifying / locked: sample_head Interviewer questions: correct_household head_still_correct current_head Enable current_head when: head_still_correct == 2
This preserves what the sample said and separately records what field staff found. That is usually analytically cleaner than overwriting the sample value.
Pattern C β let the interviewer correct the value directly
If a value truly should be editable, do not preload it as an identifying question. Instead, preload it into an interviewer-scope question using advanced preloading, or keep the original in a hidden field and collect an editable current value separately.
hidden: sample_phone interviewer: phone_confirmed current_phone
Best when you need to know both the original frame value and the field correction.
interviewer question: phone_number preloaded: 0917...
The preloaded answer appears in the interview and can generally be edited, subject to question type and any special protection rules.
Identifying vs hidden vs interviewer scope
| What you need | Best Survey Solutions scope | Behavior |
|---|---|---|
| Show household/sample information on interviewer dashboard | Identifying | Visible on assignment cards; preloaded values are locked |
| Keep original frame value for logic but do not show it | Hidden | Can be preloaded; interviewer does not answer it |
| Show a preloaded value and allow field correction | Interviewer | Preloaded in advanced mode; interviewer can interact with it unless protected by a specific rule |
| Ask supervisor-only verification after completion | Supervisor | Hidden from interviewer/respondent; available during review |
What file format does Survey Solutions actually accept?
.tab. It does not directly ingest your working .xlsx, .csv, or .dta sample file for this workflow. The official documentation explicitly says that assignment uploads rely on an external tab-delimited .tab file. Batch assignments β| Your source file | Can HQ upload it directly as an assignment/preload file? | What you do |
|---|---|---|
.xlsx / Excel | No | Save/export the required sheet as tab-delimited text, then use the resulting .tab file. |
.csv | No for this upload workflow | Convert/export it to tab-delimited text. |
.dta / Stata | No | Use Stata to export the sample in tab-delimited format. |
.tab | Yes | Upload directly after checking variable names and formats. |
.tab file against the questionnaire. The Survey Solutions team explicitly notes that Stata, SPSS, R, Excel and other packages can prepare assignment files as tab-delimited output. Assignment files βSo what should your workflow look like?
master_sample.xlsx, sample.dta, database table, etc.For example
hhid, province, municipality, barangay, sample head, address, _responsible, _quantity.Create something like
household_assignments.tab.HQ validates column names and values against the questionnaire.
The identifying values are copied into each assignment; HQ is not maintaining a live connection to your original Excel/Stata file.
Excel example
Your working file might be:
master_sample.xlsx Sheet: SurveySample hhid | province | municipality | barangay | hh_head | address | interviewer
Before upload, create/export:
household_assignments.tab hhid province municipality barangay hh_head address _responsible _quantity PH001 Cavite Dasmarinas Paliparan III Maria Santos Blk 4 Lot 12 int_ana 1
In Excel, the practical equivalent is saving a worksheet as Text (Tab delimited). If Excel gives the file a .txt extension, the content is still tab-delimited; for Survey Solutions workflows, teams commonly rename/use the expected .tab extension after confirming the content is tab-separated.
Stata example
If your sample is maintained as sample.dta, you can create the upload file from Stata rather than manually passing through Excel:
use "sample.dta", clear
keep hhid province municipality barangay hh_head address interviewer
rename interviewer _responsible
gen _quantity = 1
export delimited using "household_assignments.tab", ///
delimiter(tab) replace
The important point is the output format: tab-delimited text. Survey Solutions' own guidance notes that statistical packages can generate these files directly. Assignment files β
CSV example
If the sampling team gives you sample.csv, you can open it in Excel/Stata/R/Python and export a tab-delimited version. Headquarters does not need to know that the source was CSVβthe only thing it sees is the final .tab upload.
Why Survey Solutions uses .tab
The platform moved preloading, batch operations, and long lists to tab-delimited text specifically because it handles scripts and punctuation more reliably across languages. Preload files β
Basic sample preload: identifying data only
For a normal cross-sectional survey with a known sample, this may be all you need:
hhid province municipality barangay hh_head address _responsible _quantity PH-001-0001 Cavite Dasmarinas Paliparan III Maria Santos Blk 4 Lot 12 int_ana 1 PH-001-0002 Cavite Dasmarinas Paliparan III Jose Reyes Blk 5 Lot 8 int_ana 1
_quantity = 1, each sampled household can generate one interview. If you assign the row to a supervisor instead of an interviewer, the supervisor can distribute it to someone on their team.What if the sample is defined at EA/barangay level rather than household level?
Assignments can also represent a quota or area workload. For example:
province Cavite municipality Dasmarinas barangay Paliparan III _responsible sup_rosa _quantity 12
This means: collect up to 12 interviews under this assignment. Household-specific fields such as exact address and household head can be left blank and completed by interviewers. Survey Solutions explicitly supports this less-specific assignment model as well as one-assignment-per-household samples.
Advanced preloading: more than just the Cover page
Suppose this is a panel survey and you know much more from the previous wave:
- household demographics;
- member names, ages, and sex;
- plots operated last wave;
- previous phone numbers;
- baseline treatment group;
- previous-wave IDs.
Survey Solutions can preload data into questionnaire questions and rostersβnot only identifying questions. Headquarters provides an advanced preload template as a ZIP containing one tab-delimited file for each hierarchical level.
preload.zip
ββ Household.tab
β Id | hhid | province | sample_head | treatment | ...
β
ββ members.tab
β Id | ParentId | member_name | age | sex | ...
β
ββ plots.tab
Id | ParentId | plot_name | area | ...
Id and ParentId link lower-level records back to the household. Nested rosters can require additional parent identifiers. This is the mechanism you would use to preload last-wave household members or other repeated records.
Panel-survey example: preload last-wave members
| Id | ParentId | member_name | age | sex |
|---|---|---|---|---|
| 0 | 1 | Maria Santos | 46 | 2 |
| 1 | 1 | Paolo Santos | 22 | 1 |
| 2 | 1 | Ana Santos | 17 | 2 |
The current interview can start with these roster rows already populated. You can then ask whether each member is still in the household, update allowed information, and add new members.
Protecting preloaded roster triggers
For panel surveys, you may want to prevent an interviewer from deleting last-wave roster members while still allowing new members to be added. Survey Solutions provides protection for preloaded trigger questions of certain types:
- numeric trigger: interviewer may increase but not reduce the value;
- text-list trigger: interviewer may append items but not remove preloaded items;
- multi-select trigger: interviewer may add selections but not remove protected preselected ones;
- yes/no multi-select: preloaded Yes/No selections can be protected.
protected__variables.tab file.Example protected__variables.tab
variable__name hhmembers n_plots crops
Household location and GPS
If you already have target coordinates, they can be preloaded for navigation. Survey Solutions supports showing the assigned location to the interviewer from the assignment card so they can open it in an external navigation app. If coordinates are collected during the interview instead, those can be used later for verification.
Recommended pattern for your exact use case
If you receive a fixed household sample with province, municipality, barangay, household head, address and household ID, I would structure it like this:
- Make
hhid, province, municipality, barangay, sample household head and sample address identifying questions. - Batch upload one assignment per household with
_quantity = 1. - Use
_responsibleif the sample is already allocated to supervisors/interviewers. - Do not ask the geography and sample ID again. Show them as locked reference information.
- Add an interviewer question such as
correct_household: βAre you at the household described on the Cover page?β - For fields that can legitimately changeβhousehold head, phone, address detailβkeep the sample value and separately ask whether it is still correct.
- Enable correction questions only when the interviewer says the preloaded value is no longer correct.
- If you have previous-wave roster data, use advanced ZIP preloading and consider protecting the roster trigger so prior members cannot simply disappear.
A full example questionnaire pattern
Cover (Identifying / locked if preloaded)
ββ hhid
ββ province
ββ municipality
ββ barangay
ββ sample_head
ββ sample_address
Verification (Interviewer)
ββ correct_household
ββ head_still_correct
ββ current_head
β enable when: head_still_correct == 2
ββ address_still_correct
ββ corrected_address
enable when: address_still_correct == 2
Household roster
ββ members
ββ name
ββ age
ββ sex
ββ ...
Why this is better than asking everything again
You do not re-ask fixed sample-frame information merely to reconstruct what the project already knows.
The interviewer can see the intended household and verify they are at the right place before proceeding.
You retain the original sample-frame value separately from any corrected/current information collected in the field.
Common mistakes
| Mistake | Why it causes trouble | Better approach |
|---|---|---|
| Make every preloaded value Identifying | Preloaded identifying answers are locked | Use identifying only for stable case-reference fields; use separate confirmation/correction questions for changeable information |
| Ask interviewer to select a case ID from thousands of households | Creates avoidable lookup and selection risk | Assign each sampled household directly |
| Overwrite the frame value when correcting it | You lose the distinction between sample-frame and field-observed information | Preserve original + collect current/corrected value separately |
| Use one assignment per EA when the sample is already household-specific | Weakens household-level control and case tracking | Use one assignment per household with quantity 1 |
| Preload a roster without considering deletion | Prior-wave members can be removed | Use trigger protection when appropriate and collect status/change information explicitly |
hhid, province, municipality, barangay, household head, phone number, address, GPS, and assigned interviewer.
- Which variables would you put on the Cover page as Identifying?
- Which values might you preserve as original sample values but verify separately?
- What should
_quantitybe if each household should be interviewed once? - Would you make the enumerator choose
hhidfrom a 2,500-item list?
Show suggested answer
1. At minimum hhid plus the stable geography/address information needed to find the household; household head can also be identifying if you want the sample value locked and visible. 2. Head name, phone, detailed address and possibly GPS are good candidates for βsample value + confirmation/current valueβ patterns because they may change or be imperfect. 3. 1. 4. Noβthe assignment already represents the sampled household and should be sent directly to the responsible fieldworker/team.
Official documentation for this module
Read familiar SurveyCTO operations as Survey Solutions design decisions
Examples below are traced to the supplied ICM Integrated Survey workbook. Source excerpts are exact cell contents, not endorsements of their logic. Target examples are teaching designs that require Designer compilation and Android/web testing; no full instrument conversion or runtime equivalence is claimed.
Separate preloaded attributes from categorical labels
SurveyCTO source cells and expressions
survey!V29 Β· icm_part_name (calculate)
pulldata('icm_part_preloads', 'icm_part_og_name1', 'id',${caseid})
survey!V41 Β· pull_hh_head (calculate)
pulldata('cases_icm', 'hh_head', 'id',${caseid})
survey!V47 Β· calc_hh_mem_name1 (calculate)
pulldata('psps_baseline', 'calc_hh_mem_name1', 'caseid', ${caseid})
survey!V886 Β· fd_cat (calculate)
pulldata('preload_fd_cat', 'fd_cat_name', 'id_key', index())
survey!V895 Β· fd_cons (calculate)
pulldata('preload_fd_cat', 'fd_cons_name', 'cons',${fd_cons_val})The same pulldata function is used for household attributes, baseline names, and food labels. These require different target designs. Questionnaire-local worksheets are evidence of reference data, but do not prove which attached CSV or server dataset was deployed.
Household attributes and baseline members β assignment/roster preloading. Static food codes and translated labels β categories and translations. Static numeric parameters β numeric lookup table if representable. Mutable cases/status publishing β separate operational integration.
Reference guides: SurveyCTO expressions; calculation timing; Survey Solutions rosters; numeric lookup tables.
_responsible is a login and assigned_interviewer_name is the full display name before a synthetic upload. Field workflow: HQ β Supervisor β Interviewer
Survey Solutions treats supervision as a first-class workflow instead of an optional layer around submissions.
HQ creates/imports questionnaire
β
HQ creates assignments
β
Supervisor distributes work
β
Interviewer synchronizes
β
Interviewer conducts interview
β
Complete
β
Supervisor review
β reject approve β
Interviewer HQ review / final workflow
Think in states, not files
A field problem may be caused by responsibility, status, assignment availability, device synchronization, or questionnaire versionβnot just βthe form is missing.β
The interview status lifecycle
The status tells you who must act next. Dashboards may display the labels with spaces while APIs and exports may use compact names.
How review should work in practice
- Filter for completed interviews.
- Review invalid, unanswered, flagged and supervisor-scope questions.
- Add question-level comments that state the issue and requested correction.
- Reject when field follow-up is needed; approve when the interview is coherent and sufficiently documented.
- Review interviews approved by supervisors.
- Apply central consistency, sample and protocol checks.
- Reject to the supervisor rather than bypassing the field chain.
- Approve only when the interview is ready for the project's accepted-data workflow.
Reassignment and absence
Responsibility and interview status answer different questions. Responsibility says who currently owns the work; status says what stage it has reached. If an interviewer becomes unavailable, reassign only after checking whether the interview exists on a device, has synchronized changes, or is already awaiting review. Coordinate a final synchronization whenever possible before moving responsibility.
- Confirm the assignment and interview IDs.
- Confirm the questionnaire version.
- Check current status and responsible user.
- Ask the original interviewer to synchronize if the device contains newer work.
- Reassign through the appropriate supervisor/HQ control.
- Have the new interviewer synchronize and verify the case before travel.
- Record the reason in the fieldwork issue log.
102, assignment A-102, interviewer int_maria, questionnaire HH_v5. Your check order should explicitly cover: server/account β questionnaire version β assignment responsibility β interview status β last successful synchronization β pending rejected work β device state.Interviewer App vs Web Interviewer
This is the closest Survey Solutions analogue to the distinction between SurveyCTO Collect on Android and conducting interviews through a browser. The tablet workflow is built around synchronization; the web workflow works directly against the server.
First: interviewer accounts are not email logins
enum001, ana_01, or supnorth. Email is optional profile information, not the login itself. Current batch-user documentation says login names must be 3β15 characters and can contain Latin letters, digits, and underscores.| Field | Example | Required? |
|---|---|---|
| Login | enum001 | Yes |
| Password | Fieldteam2026A | Yes |
| Role | Interviewer | Yes |
| Supervisor | sup01 | Yes for interviewer account |
| Full name | Ana Reyes | Optional |
| [email protected] | Optional | |
| Phone | 639171234567 | Optional |
The server can create users manually or in batch. Current Survey Solutions supports creating up to 10,000 users from one tab-delimited file containing login, password, role, andβwhen relevantβsupervisor. Official batch-user documentation β
How the tablet is configured
The Interviewer App needs three things the first time it connects:
- Server address β the Survey Solutions server URL / synchronization point.
- Interviewer login β e.g.
enum001. - Password β the password assigned to that interviewer account.
https://survey.example.orgUser:
enum001Server URL
Username
Password
Receive questionnaire + assignments
Send completed interviews
Find the Interviewer App login QR code
The Users β enum_01 β Manage account page is the wrong screen for this QR code: it edits contact details, password and account settings. The login QR is on the workspace interviewer profile. In the PDS interface shown below:
- Enter Default Workspace and open Reports β Devices/Interviewers.
- Find the
sup_trainteam and click the interviewer login, such asenum_01. - On the
enum_01interviewer profile, scan the QR code at the upper right from the Interviewer Appβs first-login screen. - The scan fills the server address and interviewer login. Enter the interviewerβs password separately, sign in, then synchronize.
enum_01 under the sup_train team to open the interviewer profile.enum_01 and pre-fills the server and login. It does not contain a password.| Operational need | SurveyCTO Collect | Survey Solutions Interviewer |
|---|---|---|
| Receive a new or updated questionnaire | Get Blank Form, or configured automatic form updates | Synchronize |
| Receive updated assigned cases/work | Manage Cases β Refresh, or configured automatic case updates | Synchronize receives assignments |
| Send completed data to the server | Send Finalized Form, or configured automatic sending | Synchronize sends completed interviews and other interview changes |
The labels differ, but both require the device to contact the server. SurveyCTO exposes separate retrieve/send controls and can automate them; Survey Solutions presents one named synchronization cycle. SurveyCTO Collect workflow β SurveyCTO case refresh β
What synchronization actually does
Official documentation describes synchronization as a two-way exchange: the interviewer receives assignments/instruments and sends completed work back. Internet is needed for synchronization, but not for conducting ordinary interviews once the data are on the tablet. Synchronization documentation β
Read the tablet dashboard as four work queues
The dashboard is the interviewer's operational inbox. Each tab answers a different question, and a case can only be acted on correctly when the interviewer understands which queue it is in.
| Dashboard queue | What it contains | Correct next action |
|---|---|---|
| Create New | Assignments for which another interview may be created. Cards show the assignment number, questionnaire version and identifying information. | Confirm the case identifiers before selecting Start New Interview. Fill any identifying fields that HQ deliberately left open. |
| Started | Interviews created on this device but not yet marked complete. | Select Open and resume. Previously recorded answers remain available. |
| Completed | Interviews marked complete on the device. They remain here until synchronization transfers them to the server. | Reopen if a correction is needed before transmission; otherwise synchronize and confirm the upload result. |
| Rejected | Interviews returned by the supervisor with issues, questions or requested corrections. | Open the case, follow the question links and comments, make or explain each correction, complete again, then synchronize. |
Interviewer dashboard and workload β
A defensible interview close-out routine
Train interviewers to use the same sequence before every submission. This reduces avoidable rejections and makes genuine exceptions easier for supervisors to interpret.
Check the assignment number and identifying information before final review.
Review the counts and links for unanswered questions and answers with errors.
Correct mistakes. If an unusual answer is confirmed, retain it and leave a concise question-level comment explaining how it was verified.
For rejected interviews, address every supervisor or HQ issue instead of merely opening and completing the case again.
After completion, synchronize and read the result: interviews uploaded, new assignments received, rejected interviews returned and work removed after reassignment.
Actions that need extra care
If a list creates roster rows, removing a list item can remove the associated person's or item's roster data. Verify the intended row before deleting it.
Discard is destructive and the interview cannot be recovered from the device. It should follow an explicit project protocol rather than serve as routine troubleshooting.
A new GPS reading replaces the earlier reading. Re-record only when the first fix is inadequate and allow enough time for the accuracy to improve.
Question-level comments β Β· Question types and list behavior β Β· Validation and critical checks β
Typical field workflow
enum001, role Interviewer, attached to sup01.Households assigned to
enum001 or to the supervisor/team.Server URL + login + password, or QR code + password.
Questionnaires and assignments download to the tablet.
Complete household visits without continuous internet.
Completed interviews go back to the server and new/rejected work comes down.
Password behavior worth knowing
If an interviewerβs password was assigned by someone else, current Survey Solutions can require the interviewer to change it on first login. Interviewers can change their own password, but the system does not reveal existing passwords in clear text later; forgotten passwords are reset. Password documentation β
Tablet Interviewer vs Web Interviewer
| Feature | Android Interviewer App | Web Interviewer |
|---|---|---|
| Device | Android tablet/phone | Any supported browser |
| Internet needed during interview? | No; mainly needed for synchronization | Yes; continuous connection required |
| Where interview lives while being filled? | Primarily on device until synchronization | On server as work proceeds |
| Assignments/dashboard | Downloaded during sync | Read directly from server |
| Best fit | Face-to-face CAPI, low-connectivity fieldwork | CATI, office-based interviewing, connected devices |
| Question-type differences | Some web table/matrix presentation falls back to standard tablet presentation | Some device-dependent question types have limitations |
Web Interviewer workflow
assignment β
enum001open server URL
login as
enum001no synchronization cycle needed
Which should you choose?
Default to Android Interviewer, especially where connectivity is intermittent.
Web Interviewer is often convenient because interviews are immediately visible on the server.
Survey Solutions can support both, but test question types and workflows in each mode before fieldwork.
ana_01. What does Ana need on a fresh Android tablet?
Show answer
The Interviewer App, the Survey Solutions server URL/synchronization point, her login ana_01, and her password. A QR code can prefill the server URL and login, but not the password. She then signs in and synchronizes to receive her questionnaire and assignments.
Official documentation for this module
Exports, roster files, paradata and QA
This is another major mental shift from SurveyCTO: Survey Solutions exports data by level of observation. The household-level file is separate from each roster-level file, which means roster data are naturally long rather than flattened across the household record.
Which export formats are available?
.dta), SPSS (.sav), and tab-delimited text (.tab). Survey Solutions does not provide a native XLSX main-data export and does not use CSV as its primary main-survey export format. If you need CSV/XLSX, convert the tabular or statistical export afterward.| Export | Format | Action |
|---|---|---|
| Main survey data | Stata .dta | |
| Main survey data | SPSS .sav | |
| Main survey data | Tabular .tab | |
| Binary data | images/audio/files | |
| Paradata | paradata.tab + metadata |
Official Survey Solutions guidance explicitly recommends downloading all main-data formats when archiving a project. Data export documentation β
Wide vs long: Survey Solutions is hierarchical by default
household.dta interview__id hhid province hhsize income ...
members.dta interview__id members__id member_name age sex ...
age_1 age_2 age_3 ... for questions inside the roster. The source question that created the roster can still appear at household level in a wide form, depending on roster type.Concrete example
Suppose your questionnaire contains:
Household ββ hhid ββ province ββ hhsize ββ members ββ member_name ββ age ββ sex
With household PH001 containing three members, the export looks conceptually like:
| interview__id | hhid | province | hhsize |
|---|---|---|---|
| abc123... | PH001 | Cavite | 3 |
| interview__id | members__id | member_name | age | sex |
|---|---|---|---|---|
| abc123... | 0 | Amina | 42 | 2 |
| abc123... | 1 | Bilal | 18 | 1 |
| abc123... | 2 | Sana | 11 | 2 |
The household identifier appears in every level needed for merging. Survey Solutions uses stable system identifiers such as interview__id and roster-specific identifiers based on the roster variable name, such as members__id. The roster variable-name limit historically reflects Stata's variable-name length constraints. Variable names β
What happens to the roster source question?
The source question that created the roster is exported at the parent level:
| Roster source | Parent-level export | Roster-level export |
|---|---|---|
| Numeric roster | One variable, e.g. hhsize = 3 | Three member rows |
| List roster | Source list can generate several columns up to its configured maximum | One row per list item |
| Multi-select roster | Source multi-select exports its answer-option columns | One row per selected option |
| Fixed roster | No separate source question | One row per fixed roster item |
This is why the export is not simply βall longβ or βall wide.β The source question can have a wide representation at the parent level, while questions inside the roster are stored in a separate long roster file. Official roster-export anatomy β
SurveyCTO comparison
| Feature | SurveyCTO | Survey Solutions |
|---|---|---|
| Repeat export | Can work with wide naming patterns such as repeated-instance columns depending on export/workflow | Roster-level files are the native structure: one row per occurrence |
| Typical repeated variable names | May appear as instance-specific names such as age_1, age_2, etc. in a wide representation | age remains age in the roster file; row identity is carried by members__id |
| Main case identifier | Often your key + SurveyCTO system identifiers | interview__id plus your own identifying variables |
| Merge key for roster | Depends on export design | interview__id + roster identifier(s) |
Nested rosters
Nested rosters create additional levels/files. For example:
Survey Solutions includes the identifiers required to link each level back to its parent. The general rule is: one export file per level of observation.
Questionnaire variable name controls the main filename
Designer distinguishes the human-readable questionnaire name from the questionnaire variable. That variable is used to name the main data file during export. For example:
Questionnaire name: Philippines Household Remittance Survey 2026 Questionnaire variable: ph_remit Export: ph_remit.dta members.dta transfers.dta ...
Survey Solutions introduced this so exported filenames remain stable and language-independent. Questionnaire variable β
System-generated QA files come with the export
Each export archive also contains system-generated data files in the same format as your main export. For a Stata export, for example, these QA files are also Stata files:
These files document operational events such as assignment creation/reassignment, interview status changes, comments, diagnostics, and validation errors. System-generated export files β
Paradata: a separate event-level dataset
Survey Solutions automatically produces paradata. Each row is an event rather than a respondent or roster record.
| interview__id | order | event | responsible | timestamp_utc | parameters |
|---|---|---|---|---|---|
| abc123... | 1 | SupervisorAssigned | sup01 | 08:01:12 | ... |
| abc123... | 2 | AnswerSet | enum001 | 08:13:44 | age||42||... |
| abc123... | 3 | Completed | enum001 | 09:04:10 | ... |
The paradata archive contains paradata.tab, a Stata import script paradata.do, a human-readable readme, and machine-readable JSON metadata. Survey Solutions also supports a reduced paradata export that omits some high-volume enable/disable/validation events. Paradata format β
What paradata can tell you
Answer changes, reassignment, approvals, rejections, comments, recalculated variables.
UTC timestamp plus time-zone offset, event order, completion and review timing.
Roster addresses contain row codes, so an event can be tied to a particular member/job/plot occurrence.
Useful QA patterns
- Interview duration: derive active timing from paradata/diagnostics rather than only start/end timestamps.
- Back-check suspicious edits: inspect repeated answer changes or late corrections.
- Supervisor turnaround: use interview/assignment action files to measure time from completion β review β approval/rejection.
- Enumerator monitoring: compare interview counts, duration distributions, error frequency, GPS or audit indicators where applicable.
- Questionnaire debugging: use
interview__errorsand paradata events to locate recurring validation problems.
If you really need a wide household file
Survey Solutions does not natively flatten roster questions into age_1 age_2 age_3 .... If a downstream deliverable requires that structure, reshape it yourself after export.
hhid members__id age PH1 0 42 PH1 1 18 PH1 2 11
hhid age_0 age_1 age_2 PH1 42 18 11
Export workflow
Exports are version-specific.
For example Completed or Approved by Headquarters.
Main file + roster files + system-generated QA files + metadata.
Full or reduced event set.
PH1 has three members. Where would you expect hhid, age, and the member row identifier?
Show answer
hhid appears in the household-level file. age appears in the members roster-level file, one row per member. Each roster row carries a roster identifier such as members__id, and the roster file also carries the interview identifier needed to merge it back to the household.
Applied QA lab: build a daily fieldwork review
Use the main interview file, interview__actions, interview__errors, and paradata to produce one row per interview with:
- responsible interviewer and supervisor;
- first answer time, completion time, and an active-duration proxy;
- number of validation errors and answer changes;
- completion-to-supervisor-review time;
- rejection count and latest status;
- GPS or audit indicators required by the study protocol.
* Illustrative Stata workflow β adapt names to the exported metadata use interview__actions.dta, clear sort interview__id date by interview__id: egen first_complete = min(cond(action=="Completed", date, .)) by interview__id: egen first_review = min(cond(inlist(action,"ApprovedBySupervisor","RejectedBySupervisor"), date, .)) gen review_hours = (first_review-first_complete)/(60*60*1000) * Flag for review; do not automatically accuse an interviewer gen rapid_review = review_hours < 0.25 if !missing(review_hours)
Official documentation for this module
members roster rows. Check that the case ID links them. REST API: connecting Survey Solutions to other surveys and external data systems
Survey Solutions' REST API is the main bridge when you need automation that goes beyond the standard Designer β Headquarters β Interviewer workflow. This is where you can build cross-questionnaire pipelines, create assignments programmatically, automate exports, monitor interviews, and connect Survey Solutions to an external database or statistical system.
pulldata() across live surveys. Instead, an external process can read data from Questionnaire A or another database, transform it, and use the Survey Solutions API to create/update work for Questionnaire B.What the API is
An API, or application programming interface, is a controlled way for one computer program to ask another computer program for information or to perform an allowed action. When you use Headquarters yourself, you click buttons such as Create assignment or Export. With an API, a script sends the equivalent structured request to the server and receives a structured response that it can record and process automatically.
REST is one common style for that conversation. It combines a server address with an action word. A GET request normally asks for information, a POST request normally creates or starts something, and a PATCH request changes part of an existing resource. Data commonly travels as JSON, a structured text format made of named fields and values. The server returns a status code and usually a response body: for example, success with the new assignment ID, or an error explaining which required field was missing.
| API term | Plain-language meaning | Survey Solutions example |
|---|---|---|
| Client | The software making the request | Your Python, R, PowerShell, or integration-service script |
| Server | The system receiving and processing the request | Your Survey Solutions Headquarters server |
| Endpoint | The published address for one kind of operation | /api/v1/assignments |
| Method | The requested action | GET, POST, or PATCH |
| Request body | The structured details sent to the server | Questionnaire ID, responsible interviewer, quantity, and identifying answers |
| Response | The server's result | A status plus the created assignment ID or an error message |
| Authentication | Proof of which system account is calling | Dedicated API username/password or bearer token |
You do not need to become a software engineer to follow this module. Read each example as a workflow: who is asking β which operation is requested β what data is sent β what the server returns β what should happen next. For a gentle introduction, see MDN's overview of HTTP β and MDN's REST glossary β. Then use the Survey Solutions API overview β and your own server's Swagger page for the exact operations and fields.
Every Survey Solutions server exposes API endpoints. Current Survey Solutions provides both REST and GraphQL interfaces. The REST API can perform many of the same operational actions available in the user interface, including creating assignments, listing interviews, approving/rejecting interviews, creating users, and generating exports.
List assignments
Create new assignment
Get interview answers
Start an export job
Download completed export
Always use the API documentation from your own Survey Solutions server. Survey Solutions explicitly warns that API syntax can change between versions. On a server, the interactive REST reference is available under its API documentation pageβfor example the World Bank demo server exposes Swagger at /apidocs/index.html.
Useful links
API account and authentication
For system-to-system integration, the server administrator creates an API User. That account is intended for scripts/services, rather than normal interactive fieldwork.
API username + API password
Your HTTP client sends those credentials with each request.
Authorization: Bearer YOUR_TOKEN
Survey Solutions also supports token/JWT authentication when the server administrator has enabled it.
Usage 1 β dynamic data shared between questionnaires
Suppose you have two surveys:
- Questionnaire A: Household listing collects household ID, household head, phone number, household size, and eligibility.
- Questionnaire B: Main household interview should only be sent to eligible households and should already know the information collected in A.
In SurveyCTO you might solve this with publishing to a server dataset and then letting Form B read that dataset. Survey Solutions does not have that direct built-in cross-questionnaire lookup. The API gives you a different architecture:
Listing interviews completed
read β filter β transform
created through REST API
Example: listing β eligible main-survey assignment
listing_id, hhid, hh_head, phone, hhsize, eligible.
Your script can retrieve interviews through the interview endpoints or, for large recurring workflows, trigger a Survey Solutions export via the export API and read the resulting data file.
Keep only households where
eligible == 1. Optionally merge in other data from a CRM, administrative database, earlier wave, or sampling database.
Send the selected household data to
POST /api/v1/assignments.
The new main-survey assignment appears automatically once it is assigned and the interviewer synchronizes.
| HHID | Listing status | Eligible | Main assignment |
|---|---|---|---|
| PH001 | Completed | Yes | Created #4521 |
| PH002 | Completed | No | Not created |
| PH003 | Completed | Yes | Created #4522 |
Illustrative Python workflow
The exact request/response schema should be copied from the Swagger documentation on your project server. The example below shows the overall structure of the workflow:
import requests
SERVER = "https://survey.example.org"
USER = "api_user"
PASSWORD = "********"
# Example: create one eligible household assignment
payload = {
"Responsible": "enum001",
"Quantity": 1,
"QuestionnaireId": "QUESTIONNAIRE_GUID$VERSION",
"IdentifyingData": [
{"Variable": "hhid", "Answer": "PH001"},
{"Variable": "hh_head", "Answer": "Maria Santos"},
{"Variable": "phone", "Answer": "09171234567"},
{"Variable": "hhsize", "Answer": "5"}
]
}
r = requests.post(
f"{SERVER}/api/v1/assignments",
auth=(USER, PASSWORD),
json=payload,
timeout=60
)
r.raise_for_status()
assignment = r.json()
print(assignment)
POST /api/v1/assignments is current, but use your own server's Swagger page to confirm the exact body fields and formats for your installed version before writing a live integration.Can Questionnaire B receive more than just identifying data?
Yes, Survey Solutions supports preloading beyond Cover/identifying questions, including hidden/interviewer questions and roster data. For complex panel/cross-questionnaire preloading, however, the structure is more involved because roster questions need question identities and roster vectors. In many projects the easiest reliable pattern is:
Use the assignment API for identifying/preloaded values needed to create the case and route it to a user.
Generate the advanced preloading files from your external system, or build an API layer that understands the questionnaire document and roster identities.
The API can retrieve the questionnaire document through GET /api/v1/questionnaires/{id}/{version}/document, which is useful when an integration needs question identities and hierarchy rather than only variable names.
External dataset β Survey Solutions
The same architecture works when the source is not another Survey Solutions questionnaire:
SQL Β· CRM Β· MIS Β· Stata Β· administrative register
select cases
map fields
validate
create assignments through REST API
The World Bank's own API documentation specifically describes this type of use: an external system can consume administrative databases, remote sensing, earlier surveys, or other sources and supply the selected data to Survey Solutions as assignments.
Usage 2 β sample/case data known before fieldwork: create assignments by API
Suppose your master sample is in a SQL database or a Stata/CSV pipeline rather than a .tab file. Instead of exporting a batch assignment file and uploading it manually, your system can create assignments directly.
10,000 households
one API request per household
or controlled batches
case information + responsible user
Example sample record
{
"hhid": "PH-001-0001",
"province": "Cavite",
"municipality": "Dasmarinas",
"barangay": "Paliparan III",
"hh_head": "Maria Santos",
"address": "Blk 4 Lot 12",
"interviewer": "enum001"
}
Map the external fields to Survey Solutions variables
| External database column | Survey Solutions variable | Assignment use |
|---|---|---|
hhid | hhid | Identifying |
province | province | Identifying |
municipality | municipality | Identifying |
barangay | barangay | Identifying |
hh_head | sample_head | Identifying / reference |
address | sample_address | Identifying / reference |
interviewer | Responsible login | Routing, not questionnaire data |
Illustrative batch loop
for case in sample_records:
payload = {
"Responsible": case["interviewer"],
"Quantity": 1,
"QuestionnaireId": QUESTIONNAIRE_ID,
"IdentifyingData": [
{"Variable": "hhid", "Answer": case["hhid"]},
{"Variable": "province", "Answer": case["province"]},
{"Variable": "municipality", "Answer": case["municipality"]},
{"Variable": "barangay", "Answer": case["barangay"]},
{"Variable": "sample_head", "Answer": case["hh_head"]},
{"Variable": "sample_address", "Answer": case["address"]}
]
}
response = requests.post(
SERVER + "/api/v1/assignments",
auth=(API_USER, API_PASSWORD),
json=payload
)
# Save Survey Solutions assignment ID back to your master sample
save_assignment_id(case["hhid"], response.json()["Id"])
hhid remains the authoritative sample key, while the Survey Solutions assignment ID is useful for API updates, reassignment, history checks, and audit.A robust assignment automation table
In your external database, I would maintain fields like:
| hhid | suso_assignment_id | questionnaire_version | responsible | api_status | last_sync |
|---|---|---|---|---|---|
| PH001 | 4521 | 3 | enum001 | created | 18 Sep 10:12 |
| PH002 | 4522 | 3 | enum001 | created | 18 Sep 10:12 |
| PH003 | β | 3 | enum002 | error | 18 Sep 10:12 |
Why store the assignment ID?
Once an assignment exists, the API exposes endpoints such as:
Prevent duplicate assignment creation
Do not blindly rerun a script that creates assignments. Make the workflow idempotent:
- Before creating, check your external crosswalk for an existing Survey Solutions assignment ID.
- If needed, query
GET /api/v1/assignmentsor an individual assignment endpoint to reconcile server state. - Only create when no valid assignment already exists.
- Save the returned assignment ID immediately after successful creation.
- Log HTTP status, timestamp, and error message for failures.
Closed-loop example: Survey A β external check β Survey B
Survey A reaches HQ.
Reads/export data and checks
eligible == 1.Add treatment group, administrative beneficiary status, or previous-wave ID from your database.
Assign to the appropriate interviewer/team.
HH001 appears as a new main-survey case.
What this does β and does not β mean for βdynamic dataβ
| Scenario | Supported approach |
|---|---|
| Survey B needs values from completed Survey A before B starts | Yes: API/export A β create/preload assignment B |
| Survey B needs values from an external administrative database before B starts | Yes: external DB β API β assignment B |
| Survey B must query a remote REST endpoint live every time the interviewer answers a question | No built-in questionnaire mechanism. Survey Solutions questionnaires are designed to work offline in CAPI. |
| External system should react when Survey Solutions data change | Yes: poll the API/export at an appropriate frequency or build a scheduled integration workflow |
| External checker should reject/approve interviews automatically | Yes: retrieve data β run checks β use interview approve/reject API endpoints |
Automated export as part of the pipeline
The export API uses a job workflow:
/api/v2/exportstart export
/api/v2/export/{id}check job status
/api/v2/export/{id}/filedownload file
This is useful when Survey A has many completed cases and your integration wants a batch snapshot rather than making one interview request at a time.
REST vs GraphQL
Best starting point for operational actions: assignments, interviews, users, exports, approvals/rejections. This module focuses on REST.
Useful for selected query/mutation workflows where the GraphQL schema provides the operation you need. Survey Solutions exposes its schema from the server.
Use an existing client when it fits your workflow
The Survey Solutions API overview lists community-maintained clients for Stata as well as packages or examples for R, Python, PowerShell and .NET. The R package wraps many REST calls for survey management, exports, paradata and Shiny workflows. Stata users should review the listed Stata clients before writing raw HTTP calls. A wrapper saves syntax, but it does not remove the need to understand assignment identities, server versions, retries and reconciliation.
susocase package is a focused, Stata-first batch-release workflow. It does not claim to be the first or official Stata API client. It adds a durable case register, gradual sample release, one-household-to-many-questionnaire expansion, safe reruns, manual .tab delivery and API delivery under one workflow.Guided practical β release a deployment batch with susocase
Assume your master register contains 3,200 households, but the field plan releases only batch 3 today. Each selected row has a string household ID, an interviewer username and one or more comma-separated form aliases. The project map connects each alias to the questionnaire identity and the identifying questions that receive preload values.
* Preserve leading zeros when the source is CSV
import delimited using "households.csv", clear stringcols(_all)
destring release_batch, replace
* Your familiar allocation logic remains in Stata
replace users = "enum_001" if barangay_code == "B000012"
replace users = "enum_002" if barangay_code == "B000097"
* Local preview only: no Headquarters change
susocase plan if release_batch == 3, id(id) responsible(users) ///
forms(formids) batch("deployment_003")
* Choose one route after reviewing proposed_actions.csv and errors.csv
susocase apply, batch("deployment_003")
* OR: susocase export, batch("deployment_003")
susocase reconcile, batch("deployment_003")
susocase status, batch("deployment_003")
| Command | What it does | Server effect |
|---|---|---|
init | Creates the project map and local register location. | None |
auth | Stores API credentials in the operating-system credential store. | None |
doctor | Checks Python, configuration, authentication and API reachability. | Read only |
plan | Freezes only the selected observations, validates them and classifies creates, unchanged cases, reassignments and blockers. | None |
export | Creates one Headquarters-compatible .tab per questionnaire version and reserves the cases as pending manual reconciliation. | None until a person uploads |
apply | Rechecks users and questionnaire versions, then creates or safely reassigns cases through REST. | Creates/changes assignments |
reconcile | Reads assignment evidence and completes the case-to-assignment crosswalk. | Read only |
status | Exports register and operation receipts as CSV. | None |
The responsible value is an individual interviewer username. This matches Survey Solutions' native accountability model and makes each assignment's owner explicit.
Use allocation(supervisor) with a team-to-supervisor map when operational plans release work to supervisors first. The supervisor then distributes assignments to interviewers in that team.
susocase permits reassignment only when Headquarters reports zero interviews and the operator records that affected field devices synchronized. Headquarters cannot see work that remains only on an offline tablet. An assignment with an interview is flagged for review; active-interview transfer is outside the first package release.What to inspect before delivery
- Confirm the selected batch count. Unselected households must produce no planned assignment.
- Inspect creates, unchanged cases, proposed reassignments, exclusions and blockers separately.
- Check that leading-zero IDs and raw category codes survived. A value already converted to a number cannot be reconstructed safely.
- Confirm every form alias maps to the exact questionnaire GUID and version imported into this Headquarters.
- For manual delivery, remember that the files are prepared, not uploaded. Supply the returned assignment IDs during reconciliation.
- For API delivery, treat a timed-out write as unresolved. Query and reconcile before another creation attempt.
susocase status reflects server evidence. Assignment creation alone does not prove device receipt.Practical architecture I recommend
βββββββββββββββββββββββββββ
β Master / external DB β
β hhid, sample, metadata β
ββββββββββββββ¬βββββββββββββ
β
ββββββββββββββΌβββββββββββββ
β Integration layer β
β Stata / Python / R β
β validation + logs β
βββββββββ¬βββββββββ¬βββββββββ
β β
REST API β β REST/export API
β β
βββββββββββββββΌββ βββΌβββββββββββββββββ
β Survey A / HQ β β Survey B / HQ β
β listing β β main interview β
βββββββββββββββββ ββββββββββββββββββββ
Common API mistakes
| Mistake | Better practice |
|---|---|
| Using an HQ user's password in scripts | Create a dedicated API User with appropriate access. |
| Hard-coding API syntax from an old tutorial | Check Swagger on the exact server/version you are using. |
| Using Survey Solutions internal DB directly | Use supported REST/GraphQL/export interfaces; database internals can change. |
| Assuming questionnaire B can live-query questionnaire A | Use an external integration process to transfer/preload values. |
| Creating thousands of assignments without a crosswalk | Store hhid β assignment_id and log every API result. |
| Using Survey Solutions roster IDs as your panel person ID | For panel work, preload your own stable person ID in a hidden/interviewer variable. |
eligible == 1 should receive the main survey, and the main survey needs hhid, household head, phone, treatment group from an external database, and the interviewer login.
Show suggested architecture
Use an external integration script. Periodically retrieve completed listing data (interview API or export API), filter eligible == 1, merge treatment group and interviewer allocation from the external database, then create one main-survey assignment per eligible household using POST /api/v1/assignments. Save the returned Survey Solutions assignment ID against hhid so reruns do not create duplicates.
Operational API pattern: make retries safe
A field integration should be restartable. Keep a durable crosswalk and make each run reconcile desired work with actual server state before creating or changing anything.
for case in eligible_cases:
existing = crosswalk.find(case.hhid, questionnaire_version)
if existing:
reconcile(existing, server_state)
continue
response = create_assignment(case)
if response.success:
crosswalk.save(case.hhid, response.assignment_id,
questionnaire_version, "created")
elif response.is_transient:
retry_with_backoff()
else:
log_for_review(response.status, response.body)
Use bounded exponential backoff for timeouts, connection failures and eligible 5xx responses. Respect server throttling and never retry an invalid request indefinitely.
After an uncertain response, query the server and crosswalk before creating again. This prevents duplicate assignments when the server succeeded but the client missed the response.
For exports, use the server's asynchronous job pattern: request an export, store the job identifier, poll at a reasonable interval, verify completion, download once, and record the file checksum. Confirm endpoints and payloads in Swagger on the exact server version.
Official documentation for this module
- Survey Solutions API overview
- Current demo-server REST API / Swagger reference
- Basic vs bearer-token authentication
- Survey Solutions API R package
- Native tab-delimited assignment upload
- World Bank API integration examples, external databases, and closed-loop workflows
- Why integrations should use APIs/exports rather than directly accessing the Survey Solutions database
Build, deploy and test a household roster and education survey
Build your first working Survey Solutions form in small, testable passes. Start with a household ID and consent, add one roster, then layer in age, phones and education before uploading assignments.
Your build route
- Start small: save
caseidin Cover and compile once. This is your first working checkpoint. - Control the interview: add coded Yes/No consent and test both consent and refusal.
- List people: create
member_fnamesand itsmembersroster; test with two people before adding calculations. - Complete member questions: add names, relationship, age (including special values), phone and education one block at a time.
- Deploy a practice batch: compile, import the questionnaire, upload the matching TAB, and synchronize an interviewer account.
- Prove it works: run the test scenarios, review an interview and inspect the parentβroster export.
Stay on this main route for a first build. The expandable troubleshooting notes explain real errors if you encounter them; they are not prerequisites.
Embedded practice resources
These files are stored inside this HTML. They remain available when the course is copied to another computer or opened without internet access.
The CSV preserves the supplied sample. The TAB is a derived practice upload with _quantity = 1, direct assignments split evenly between enum_01 and enum_02, and the matching full name in assigned_interviewer_name.
Preview the ten synthetic households
| caseid | province | municipality | barangay | purok | treatment_arm | hh_head_name |
|---|---|---|---|---|---|---|
| HH-2024-001 | Iloilo | Pavia | Mabini | Proper | treatment | Bruno Fernandez |
| HH-2024-002 | Iloilo | Pavia | Salvacion | Sitio A | control | Harry Maguire |
| HH-2024-003 | Antique | San Jose | Catungan | Zone 2 | treatment | Patrick Dorgu |
| HH-2024-004 | Capiz | Roxas City | Poblacion | Block 3 | control | Leny Yoro |
| HH-2024-005 | Aklan | Kalibo | Linao | Purok 1 | treatment | Luke Shaw |
| HH-2024-006 | Iloilo | Miagao | Baybay | Sitio B | treatment | Mason Mount |
| HH-2024-007 | Antique | Sibalom | Mapulang Lupa | Zone 1 | control | Manuel Ugarte |
| HH-2024-008 | Capiz | Pontevedra | Centro | Proper | treatment | Godwill Kukonki |
| HH-2024-009 | Aklan | Banga | Lumangbayan | Purok 3 | control | Kobbie Maino |
| HH-2024-010 | Iloilo | Leganes | Santo NiΓ±o | Zone 3 | treatment | Matheus Cunha |
Part A β Translate the specification into Survey Solutions
1. Create the questionnaire shell and cover
Create a new Designer questionnaire named Household Roster and Education Practice with questionnaire variable hh_education_practice. Open the built-in Cover section and begin with the household identifier.
Checkpoint 1 β finish and save caseid
- Confirm that the left side of Designer says Cover. If
caseidis shown in this section, it is already an identifying question. - Set Question type to Text, Variable name to
caseid, Variable label to Household ID, and Question text to Household ID. Cover questions require a variable label because that label is used on assignment and interview cards. - Click the green SAVE button at the bottom of the question editor. Do not look for an Identifying control; none is needed.
caseid appears as a saved question in the Cover list. If Designer keeps the editor open, the absence of an unsaved-change warning and the saved item in the list are the important checks.Checkpoint 2 β compile the saved starting state
- Click COMPILE in the top toolbar.
- Read the result rather than closing it immediately. Confirm that Designer does not report a missing identifying question or a missing Cover variable label for
caseid. - If an error names
caseid, return to the question, confirm that it is inside Cover and that its variable label is filled, save again, and recompile.
Complete the rest of the Cover
Add the remaining sample-frame questions directly inside Cover, one at a time, so their variable names match the assignment file exactly. Each becomes identifying automatically. Save each question before adding the next.
| Variable | Type | Purpose |
|---|---|---|
caseid | Text | Stable household ID. This is the first saved-and-compiled checkpoint above; validate that it is not blank. |
province, municipality, barangay, purok | Text | Location shown on the assignment and interviewer dashboard. |
treatment_arm | Text | Frozen practice allocation from the sample list. |
hh_head_name | Text | Sample-frame name of the household head. Keep it separate from roster responses. |
caseid, province, municipality, barangay, purok, treatment_arm, and hh_head_name. Give every one a clear variable label, compile again, and resolve all errors before moving on.Add interview_date as an interviewer Date question with timestamp recording at the start. Do not duplicate enumerator and supervisor names merely to recover operational metadata: Survey Solutions already tracks responsible accounts and exports workflow history. Add a questionnaire name field only when the interview text or protocol needs an explicit preloaded or independently confirmed value, and document whether that value remains valid after reassignment.
2. Consent and interview outcome
Configure consent as a single-select question so the answer can control the rest of the interview. This Designer screen shows the question type and the preloaded interviewer-name substitution in place.
%assigned_interviewer_name% supplies the preloaded full name. Check the Yes/No category codes below the visible part of the editor, and replace the remaining [organization] prompt with the real organization name before deployment.Checkpoint 3 β make consent a coded Yes/No question
- Confirm the question type is Single-select categorical and that its categories are Yes =
1, No =2. The screenshot shows the type, but the category list is below the visible area. - Rename
consent_q_01toconsentif you want to follow the expressions in this capstone exactly. If you keepconsent_q_01, replaceconsentwith that name in every later expression. - Place all substantive sections after consent under enabling condition
consent == 1. - If consent is refused, enable an outcome question such as
final_outcomeand allow the interview to end correctly. Refusal is a valid outcome, not a validation failure. - Make the consent question critical if the project requires an explicit recorded answer before submission.
Do not rely on an interviewer-name system substitution
%interviewer% and @interviewer are not supported questionnaire shortcuts for the logged-in user's name. Current Survey Solutions documentation lists %rostertitle% and %self% as the system-defined text substitutions, and Designer's expression context does not expose the HQ account's display name. The optional full name stored in an HQ user profile is operational account metadata, not a questionnaire value that can be piped automatically into consent text. Text substitution βIf an older fo_name calculation fails with WB0027
@user in a nested conditional expression. Survey Solutions does not expose @user or @interviewer as the logged-in username, so the variable cannot determine the current staff member this way.- Create the replacement field first. In Cover, add a Text question named
assigned_interviewer_name, give it a clear variable label, and save it. - Change consent. Replace
%fo_name%with%assigned_interviewer_name%, then save the consent question. - Remove the broken variable. Delete
fo_nameafter nothing refers to it. Do not replace@userwith@interviewer; both approaches are unsupported. - Compile again. For Designer testing, enter a sample name in the Cover question. For fieldwork, preload the full name through the assignment workflow explained below.
fo_name, replace its entire expression with "John Smith", save, and compile. That fixed string should compile, but it is not dynamic and must not be used in production.- Add an interviewer instruction: Introduce yourself by full name, show your ID if required, and name the organization before reading the consent statement.
- If you do not preload a name, use a stable opening such as: Good morning/afternoon. I am part of the survey team. If you do preload it, use
%assigned_interviewer_name%as shown above. - Replace
[organization]in the screenshotβs wording with the actual organization name before deployment. Treat morning/afternoon as spoken guidance rather than expecting Designer to choose it automatically.
- Create a Text question in Cover named
assigned_interviewer_namewith variable label βAssigned interviewer name.β - Add a column named exactly
assigned_interviewer_nameto the assignment.tabfile and preload the intended staff member's full display name for each case. - Use
%assigned_interviewer_name%in the consent text, then compile and test.
When you are ready to upload: how interviewer names and usernames work in the assignment TAB
_responsible versus assigned_interviewer_name
These two columns have different jobs and normally contain different forms of the same person's identity:
| Assignment-file column | What it must contain | What Survey Solutions does with it |
|---|---|---|
_responsible | The existing Survey Solutions login username, for example jolex. Do not enter βJolex Bandaβ unless that is literally the account login. | Routes the assignment to that HQ, supervisor or interviewer account. For direct interviewer assignment, use the interviewer's login. |
assigned_interviewer_name | The human-readable full name to show in consent, for example Jolex Banda. | Preloads the ordinary questionnaire question with that exact text. It does not assign or route the case. |
_quantity | An integer such as 1, or -1 for an unlimited assignment where appropriate. | Controls how many interviews may be created from the assignment. |
Because assigned_interviewer_name is placed in Cover in this exercise, it is an identifying question and should appear in the assignment template downloaded from Headquarters. Populate it in the same assignment .tab file alongside _responsible:
caseid assigned_interviewer_name _responsible _quantity HH-001 Jolex Banda jolex 1 HH-002 Alice Smith asmith 1
Read the first row as: create assignment HH-001, route it to the account whose login is jolex, allow one interview, and preload the display text βJolex Bandaβ into the questionnaire. The two name columns are deliberately not interchangeable. Assignment upload columns β
enum_01 and enum_02 correctly pair the login with Mike Johnson and Abel Kayembe. Rows 9β10 use sup_train, so βSeliano Masautsoβ would describe the supervisorβnot a guaranteed interviewer. The revised downloadable TAB assigns those final two cases directly to the interviewers instead. Also save the file as UTF-8 so Santo NiΓ±o does not become Santo NiΓΒ±o.assigned_interviewer_name may appear before or after _responsible and _quantity. Keep the headers spelled exactly as the questionnaire and Headquarters expect..tab template for that version. Do not add the column to an older questionnaire version whose schema does not contain it._responsible contains a supervisor's username, the assignment initially belongs to that supervisorβnot to the interviewer named in assigned_interviewer_name. The supervisor may later give it to a different interviewer. Survey Solutions will update operational responsibility, but it will not rewrite your questionnaire field.assigned_interviewer_name records what the assignment file said. It does not automatically change when Headquarters reassigns the case, and a preloaded Cover answer is locked. Use this design only when assignments are sent directly to known interviewers and reassignment is tightly controlled. If reassignment is common, prefer a spoken self-introduction or another field protocol that cannot display a stale name.Why a Designer lookup table cannot map hundreds of login names to full names
rowcode, and every other data column must also be numeric. Renaming interviewer_login to rowcode, adding a numeric ID, using LINQ, or switching from GetRow() to another lookup syntax cannot make a text column valid. Lookup-table rules βrowcode.tester1 cannot be used as rowcode; the key must be an integer.rowcode still fails because interviewer_login and full names are strings, while lookup-table content must be numeric.Recommended scalable workflow β preload the display name with the assignment
- Keep the questionnaire field simple. Use the Cover Text question
assigned_interviewer_namedescribed above. Do not createfo_nameas a calculated variable for this workflow. - Build assignments outside Designer. Join each sampled case to your staff directory in Excel, R, Stata, Python, or your case-management system. Put the full name in
assigned_interviewer_nameand the HQ login in the special_responsiblecolumn. - Upload the tab-delimited assignment file in Headquarters. The column names for questionnaire data must match Designer exactly. One file can cover many interviewers when each row supplies
_responsible. - Pipe the preloaded value into consent. Use
%assigned_interviewer_name%, save, compile, and test at least one assignment for each responsible account.
caseid assigned_interviewer_name _responsible _quantity DEMO-001 John Smith tester1 1 DEMO-002 Mike Johnson tester2 1
This embedded file contains one row per case, the matching Cover fields, _responsible, and _quantity. It is an assignment upload, not a Designer lookup table; adapt the sample only after your questionnaire fields match. Assignment upload guide β
Alternative β use a coded single-select question
If the project already manages a stable numeric staff code, create reusable categories such as 1 = John Smith and 2 = Mike Johnson. Bind them to a Cover single-select question named assigned_interviewer_code, preload the numeric code, and use %assigned_interviewer_code% in consent. Text substitution displays the selected option's label, not its numeric code. This avoids hundreds of C# branches, but the code-to-name category list still needs governance and the preloaded value can become stale after reassignment.
Why the proposed expression cannot work
| Expression piece | Plain-language intent | Why it fails here |
|---|---|---|
enumerators | Use the uploaded table named enumerators. | The file contains string data, so Designer rejects it before an expression can query it. |
GetRow(@interviewer) | Find the row whose key matches the logged-in account. | A lookup key must be an integer, and @interviewer is not a supported questionnaire system variable. |
?.interviewer_fn | If a row was found, read its full-name field. | A full name is text, but lookup-table data fields are numeric. |
?? @interviewer | If no name was found, fall back to the login. | The fallback operator is valid C# in the right context, but it cannot repair an invalid table or supply a nonexistent system value. |
enumerators.GetRow(@interviewer)?.interviewer_fn ?? @interviewer // Conceptually understandable, but not valid for this Survey Solutions use case.
LookupTableName[code].ColumnName, optionally guarded with LookupTableName.ContainsKey(code). In plain language: choose the table, find the integer key in rowcode, then return a numeric value from that row._responsible controls who receives the assignment; assigned_interviewer_name is ordinary preloaded questionnaire data. Reallocating an assignment does not prove that the displayed name was refreshed. Add a reconciliation step before fieldwork or use the spoken-introduction pattern when reassignment is common.1 and No stores 2, that the displayed staff nameβif you chose to preload oneβmatches the assignment, and that refusal follows the intended end-of-interview route.3. Build the household member roster
- Add list question
member_fnames: βList the first names of all current household members, starting with the household head.β - Set a realistic maximum, for example 20 items.
- Create list roster
memberstriggered bymember_fnames. - Use
%rostertitle%in member-specific question text.
The source loop asks βIs there another member?β after every person. In Survey Solutions, the list question's Add Item interaction controls row creation. This is easier to review and gives the roster stable row identities without reproducing event-driven repeat behavior.
Inside members, add middle_name, last_name, has_suffix, suffix, has_nickname, nickname and relation_to_head. Use the PDF for exact labels and categories. The examples below use these same names throughout; variable names are case-sensitive.
suffix enabled when: has_suffix == 1 nickname enabled when: has_nickname == 1
Filter and validate relationship to the household head
Because the list question instructs the interviewer to enter the household head first, code 1 (Head) should be the only relationship available in row 0, and it should be unavailable in every later row. Put this expression in the Filter field of relation_to_head:
(@rowindex == 0 && @optioncode == 1) || (@rowindex > 0 && @optioncode != 1)
Designer evaluates the filter once for each candidate category. @optioncode is that candidate's code, while @rowindex identifies the current roster position. The result is: row 0 sees only Head; rows 1 onward see every permitted relationship except Head.
If you retain a validation condition on relation_to_head, it must accept both the valid first-row case and the valid later-row case:
(@rowindex == 0 && self == 1) || (@rowindex > 0 && self != 1)
Use an error message such as: The first listed member must be the household head, and later members cannot be coded as head. The earlier expression @rowindex == 0 && self == 1 by itself is incorrect because it evaluates to false for every row after the first. The matching validation above is a defensive check; the filter already prevents the interviewer from selecting the disallowed categories.
relation_to_head as a critical question; Survey Solutions then treats each roster instance separately at completion. Mandatory questions βWhy there is no roster-level validation box
You are not overlooking a hidden control. Survey Solutions rosters support an enabling condition but do not have validation conditions. Validation rules can be attached to questions and static text, while a critical rule can be defined for the questionnaire as a whole. Roster properties β
member_fnames.Length >= 1 is an optional display guard rather than a data-quality validation.Put household-wide roster checks on members_confirm
Place the aggregate checks on a simple Yes/No confirmation question members_confirm after the roster, where the whole members collection is in scope. For a first pass, its text can simply ask the interviewer to confirm the listed members; add the generated full-name sentence in the optional second pass below. Add these as two separate validation conditions so each failure has a precise message:
Validation condition on members_confirm | Error message |
|---|---|
members.Count(p => p.relation_to_head == 1) == 1 | There must be exactly one household head. |
members.Count(p => p.relation_to_head == 2) <= 1 | There cannot be more than one spouse or partner of the household head. |
These rules are evaluated when members_confirm has an answer. Keep that question outside the roster and after all relationship questions. If these conditions must prevent interview completionβnot merely flag invalid confirmationβalso create an equivalent questionnaire-level critical rule:
members.Count(p => p.relation_to_head == 1) == 1 && members.Count(p => p.relation_to_head == 2) <= 1
Once the list, roster and relationship checks work with two members, continue to age and education. The name calculations below are a useful second pass when you want to display polished full names in the interview.
Second pass: calculate the head's full name and join every roster member's name
Create one household-level head_name
head_name is indented inside the roster, so Survey Solutions would calculate one value for every roster row. Drag it out only if Designer shows an unambiguous parent-level drop target; otherwise select it and use MOVE TO to place it in the parent household section, immediately after the roster.The list question stores objects with a numeric Value and text Text. The roster is an ordered collection. Therefore the first name entered in the list is member_fnames.First().Text, while the first roster record is members[0]. In your build, the first listed person is defined as the household head.
If the suffix is a Text question
Set head_name to type String and use this expression outside the roster:
members.Any()
? String.Join(" ", new[] {
member_fnames.First().Text,
members[0].middle_name,
members[0].last_name,
members[0].suffix_text
}.Where(x => !String.IsNullOrWhiteSpace(x)))
: null
This means: if the roster has at least one person, take the first listed first name and the first roster row's middle name, last name, and suffix; discard blank pieces; then join the remaining pieces with one space. If the roster is empty, return no value.
If suffix is categorical, as in the supplied screen
A single-select answer is a nullable integer in expressions, not its displayed label. Create a String variable named suffix_text inside the roster and map your questionnaire's actual category codes to their text. The following is a templateβreplace the example codes and labels with the values shown in your suffix question:
suffix == 1 ? "Jr." : suffix == 2 ? "Sr." : suffix == 3 ? "II" : suffix == 4 ? "III" : suffix == -666 ? suffix_oth : null
Then use the household-level head_name expression above. The practice questionnaire uses -666 for βOtherβ; if your own category code differs, change it. Do not concatenate members[0].suffix directly or the result will contain the numeric category code rather than βJr.β or βIII.β
member_fnames for the source list and members for the roster, matching the supplied Designer screenshots. If your own questionnaire uses different variable names, substitute them everywhere before compiling. Survey Solutions data types βhead_name. Also confirm that a second member does not replace the head.Worked example: build and display a list of every member's full name
This task has two calculations at different levels. First, build one clean full_name inside every members roster row. Second, place full_names_joined outside the roster and join all those row-level values into one household-level sentence. Keeping those two jobs separate makes the logic easier to compile, test and reuse.
Step 1 β build one full_name inside the roster
- Right-click the
membersroster and choose Add variable, or use the section controls and then confirm that the new variable is indented inside the roster. - Set Variable type to String and Variable name to
full_name. - Enter the expression below, save, and compile.
String.Join(" ", new[] {
member_fnames
.Where(x => x.Value == @rowcode)
.Select(x => x.Text)
.FirstOrDefault(),
middle_name,
last_name,
suffix_text
}.Where(x => !String.IsNullOrWhiteSpace(x)))
| Expression piece | Meaning in plain language |
|---|---|
x.Value == @rowcode | Find the item in the source list whose identity matches the current roster row. |
.Select(x => x.Text).FirstOrDefault() | Take that list item's displayed first name. If no matching item exists, return no first name instead of crashing the calculation. |
new[] { ... } | Place the first, middle, last and suffix text into a temporary ordered set of name parts. |
Where(...IsNullOrWhiteSpace...) | Discard missing or blank parts, so an absent middle name or suffix does not create doubled spaces. |
String.Join(" ", ...) | Combine the remaining parts with one space between them. |
@rowname. Modern Designer reports WB0276 because that variable is no longer supported. For a list-driven roster, match the supported current-row identifier @rowcode to the source list's numeric Value, then read its Text. Also use suffix_text, not the categorical suffix code, when the output should show βJr.β or βIII.β Compilation message WB0276 βfull_name is inside members. The expression on the right removes blank name parts before joining them with spaces.Step 2 β join all roster-row names outside the roster
- Add a String variable named
full_names_joinedin the parent household section, after the roster. It must not be indented as a child ofmembers. - Use the expression below. It reads the
full_namecalculated for each roster row, removes any empty result, and joins the names with a comma and a space.
String.Join(", ",
members
.Where(p => !String.IsNullOrWhiteSpace(p.full_name))
.Select(p => p.full_name))
p means one roster row at a time. Where keeps rows with a usable name, Select takes each row's full_name, and String.Join produces one display string such as John Banda, Mary Phiri Jr., Peter Mwale.Step 3 β substitute the joined result into the confirmation question
Create or edit the categorical confirmation question members_confirm outside the roster. Insert %full_names_joined% wherever the generated list should appear:
Thanks for providing me the names. Please confirm that the following members live together, eat together, and are current residentsβthat is, members who are here now or traveling for less than three months: <b>%full_names_joined%</b>.
<b>...</b> makes the generated names bold. The captured build compiles with 0 errors. Its six warnings still need to be opened and reviewed separately; zero errors does not mean warnings can be ignored.p.full_name with the row value you want to displayβfor example p.school_name or p.crop_nameβwhile keeping the sequence Where β Select β String.Join. Change the first argument from ", " to another tested separator such as "; " when semicolons are easier to read.4. Demographics and age
Add gender, dob_known, dob, age_value, age_unit, calculated variable age_years, marital_status, and first_union_age inside the roster. A single Date question cannot store partial dates and special numeric missing codes cleanly, so explicitly ask whether the full date is known and use the age fallback when it is not.
Offer βDon't Knowβ and βRefuseβ as numeric special values
For age_value, choose Numeric, check Integer, and use ADD SPECIAL VALUE to add -999 = Don't Know and -888 = Refuse to answer. These appear as selectable answers alongside ordinary numeric entry; the interviewer does not have to type the codes. Leave Non-negative unchecked when using these negative codesβotherwise Designer reports WB0324.
-999 and -888 explicitly. Do not interpret those codes as actual negative ages. Official special-values guide βValidate the numeric question age_value first. This accepts the two special codes or an ordinary age from 0 to 110:
self == -999 || self == -888 || self.InRange(0,110)
For ordinary ages, enable the categorical age_unit question. If it uses 1 = days, 2 = months and 3 = years, put this second validation on age_unit to apply the narrower day and month limits:
(self == 1 && age_value.InRange(0,29)) || (self == 2 && age_value.InRange(0,11)) || (self == 3 && age_value.InRange(0,110))
Make age_unit critical when enabled. Use an error message such as The age does not fit the selected unit. Change the unit codes and limits if your protocol differs. Keeping the first rule on age_value independent of age_unit avoids a circular dependency.
dob enabled when: dob_known == 1 age_value enabled when: dob_known != 1 age_unit enabled when: dob_known != 1 && age_value >= 0 marital_status enabled when: age_years > 13 first_union_age enabled when: marital_status >= 1 && marital_status <= 5
Use a nullable Long Integer calculated variable for age_years. With interview_date captured earlier, calculate completed years rather than dividing days by 365.25. The fallback below returns no age in years for a special value or unanswered age, zero for a valid age given in days or months, and the entered value for years:
dob.HasValue && interview_date.HasValue
? (long?)(interview_date.Value.Year - dob.Value.Year
- (interview_date.Value.Date < dob.Value.Date.AddYears(
interview_date.Value.Year - dob.Value.Year) ? 1 : 0))
: (!age_value.HasValue || age_value == -999 || age_value == -888
? (long?)null
: (age_unit == 3 ? age_value
: (age_unit == 1 || age_unit == 2 ? (long?)0 : null)))
Compile this expression in the selected Designer version and test it on the day before, the day of and the day after a birthday, as well as on leap day and for infants. Also select each special value in the Tester and confirm that age_years stays blank and age-gated questions do not appear.
5. Phone selection and member-level phone details
Create phone_status first so None, Don't know and Refuse remain explicit outcomes. When a household has phone owners, add a linked multi-select phone_owners sourced from members and limit it to three selections.
phone_owners as a roster source. In Survey Solutions, a linked multi-select question cannot trigger a roster. Keep the phone-detail questions inside the existing members roster and enable a Phone details subsection only for a selected member. This retains one stable roster identity for each person.Inside members, add phone number, network, phone type, another-SIM indicator and second number. A linked multi-select stores roster identities rather than ordinary category codes. In the member roster, test whether the current row code occurs in that identity vector:
phone_owners enabled when: phone_status == 1
Phone details subsection when: phone_owners.Any(p => p[0] == @rowcode)
second_phone enabled when: has_second_phone == 1
phone text validation:
Regex.IsMatch(self, @"^(0[0-9]{10}|-999|-888)$")
The vector shape depends on roster depth. The expression above is for this one-level roster; compile it in Designer and confirm it for selected and unselected members before deployment.
6. Education inside the member roster
Create an Education subsection inside members and enable the whole subsection with age_years >= 5. Keeping education in the existing roster avoids asking the interviewer to recreate or manually select the same eligible members.
| Variable | Condition | Implementation note |
|---|---|---|
school_attend | Education subsection enabled | Yes = 1, No = 2. |
current_grade, school_type | school_attend == 1 | Use the supplied grade and school-type codes. |
not_attend_reason | school_attend == 2 && age_years <= 24 | Ask only for non-attenders aged 5β24. |
highest_grade | All members aged 5+ | Reuse the grade codes plus 00 and 60. |
completed_level | highest_grade != 0 && highest_grade != 99 | Yes = 1, No = 2. |
Finish with open text end_comments, closing static text, variable labels, and at least one critical check for required operational fields. Compile until Designer reports zero errors, then test each branch before import.
Part B β Upload the sample and run the PDS test
Return to the expandable assignment-name note in Step 2 if you need to check the difference between _responsible (the account login) and assigned_interviewer_name (the full name displayed in consent).
- Import the compiled questionnaire to the PDS as Version 1 using the Designer credentials that own or can view it.
- Download the embedded Survey Solutions assignments TAB. Its headers match the cover variables and it contains
_responsibleand_quantity. - In Headquarters, open Survey Setup β Questionnaires. Move the mouse pointer over the imported questionnaire row so it highlights, then click the questionnaire title or row. This opens its action menu; choose Upload assignments.
- Do not proceed if verification reports a mismatch. Check variable spelling, question types, UTF-8 text and whether every uploaded field is an identifying question.
- After βVerification Complete,β create the assignments. The TAB sends five cases directly to
enum_01and five directly toenum_02; the matching display names are preloaded in the same rows. - As
sup_train, verify that both interviewers received the expected cases. Do not reassign these consent-name practice cases unless you also have an approved way to keep the preloaded display name consistent. - On Android, configure the Interviewer App with the PDS URL and
enum_01, then synchronize. In a browser, sign in asenum_02and open Web Interviewer.
_responsible.When an assignment column cannot be matched: PL0003
The upload below stopped before creating any assignments. HQ reported PL0003: Column cannot be mapped to any question in the questionnaire and identified barangay as the unmatched header. This means the questionnaire version shown on the upload screen has no matching question at the level expected by this file. In this case, a Cover question's variable name was misspelled; the same error can occur when a spreadsheet contains a column that was never added to the questionnaire.
PL0003 points to the header barangay, not to a value in a particular household row. The message No assignments were created means the file can be corrected and submitted again without duplicating this failed batch.- Compare the exact headers. Download the assignment
.tabtemplate for the same imported questionnaire version. Compare its first row with the first row of your upload, especiallybarangay. Check spelling, underscores, capitalization, and accidental spaces. Check the question's variable name in Designer, not its question text or variable label. - If the Cover variable name is wrong: correct it in Designer, save and compile the questionnaire, then import the revised questionnaire into Headquarters. Download a fresh assignment template for that new version and upload to that version. Editing Designer alone does not change an already imported Version 1.
- If the TAB contains an unwanted extra column: remove that column from the upload file and save it as tab-delimited UTF-8 text. Keep the intended Cover fields and any valid special columns such as
_responsibleand_quantity. - Retry verification. Confirm that Headquarters reports verification complete before creating the assignments. If another header is named, resolve that mismatch in the same way.
barangay column still fails against an older imported version if that version contains the misspelling. Make the assignment file and the selected HQ questionnaire version agree; use the freshly downloaded template as the reference. Official PL0003 guidance βRequired test scenarios
| Scenario | Data to enter | Expected evidence |
|---|---|---|
| Consent refusal | consent = 2 | Substantive sections remain disabled; outcome is recorded; interview can close under the chosen critical-rule policy. |
| Attending child | Head, spouse and child aged 10; child attends Grade 4. | Exactly one head; marital questions follow age; education appears for age 5+; current grade and school type appear. |
| Non-attending youth | Member aged 17 with school_attend = 2. | Reason for non-attendance appears; current grade and school type remain disabled. |
| Older non-attender | Member aged 30 with school_attend = 2. | Non-attendance reason remains disabled because age is above 24; attainment questions still appear. |
| Unknown or refused age | Choose each age_value special value: -999, then -888. | Each is a selectable, recorded answer; age_years remains blank and age-dependent questions do not appear. |
| Invalid numeric age | Try a negative ordinary age or a value beyond the selected unit's limit. | The age validation flags it; only the defined negative special codes are accepted. |
| Phone validation | Enter an invalid number, then a valid 11-digit number beginning with 0. | Validation triggers, correction clears it, and phone details exist only for selected members. |
| Roster lifecycle | Enter answers for three people, then remove the second list item using synthetic data. | Observe which roster row and answers are removed; restore the case and document why list deletion requires care. |
Required review cycle and evidence
- Complete and synchronize one Android interview.
- Complete one Web Interviewer case.
- As supervisor, leave a question-level comment and reject one interview.
- As interviewer, synchronize, correct, comment and complete again.
- Approve as supervisor, then approve as Headquarters.
- Export main and roster data in TAB or Stata format.
- Verify all ten
caseidvalues remain strings. - Join
membersto the household file using the exported parent identifier. - Confirm treatment and location preloads match the embedded sample.
- Inspect paradata for answer changes, completion, synchronization and review actions.
Independent extension: household remittance survey
Build a household survey that identifies Gulf-linked households, lists members, identifies a remittance recipient, records recent transfers, and captures sender contact permission.
Extension A β Designer
- Cover: district, EA, household ID.
- Consent section.
- Household roster: name, age, sex, relationship.
- Migration/remittance screener.
- Linked question: main remittance recipient.
- Transfer roster: amount, method, fee, exchange rate, date.
- Sender contact and permission.
- At least 5 validations and 5 enabling conditions.
Extension B β Operations
- Import questionnaire to HQ.
- Create supervisor/interviewer accounts.
- Create sample assignments with identifying data.
- Sync an interviewer device.
- Complete, reject, correct and approve one interview.
- Export main + roster data.
- Merge roster records to household identifiers in Stata/R.
- Inspect paradata for the test interview.
- Optional advanced task: design how a follow-up questionnaire would receive fresh data from this surveyβassignment preload vs lookup table vs external API/databaseβand explain why.
Final mandatory quiz
Attempts remaining: 3Answer all seven items. A perfect score unlocks course completion. After three complete attempts, the answer key is revealed and completion unlocks.
relevance maps most closely to: