surveycto-skill

内容来源:SKILL.md(标准 Skill 格式) · 原始地址 · 查看安装指南

原始内容


name: scto description: > Use this skill for ANY task involving SurveyCTO, XLSForm, ODK, KoboToolbox, or any survey form design work. Triggers on: "SurveyCTO", "XLSForm", "ODK", "CAPI instrument", "household survey", "phone survey", "CATI form", "survey form design", "field survey tool", ".xlsx form definition", "enumerator survey", "roster module", "repeat group", "skip logic", "survey instrument", "development economics survey", "impact evaluation survey", "RCT survey", "data collection form", "fieldwork instrument", "KoboToolbox", "questionnaire design", "survey programming", "pulldata", "choice_filter", "cascading select", "audio audit", "text audit", "select_one", "select_multiple", "calculate field", "constraint expression", "relevant expression". Use this skill even if the user just says "create a household survey", "add a skip logic condition", "build a roster", "write a questionnaire", or "design a form for fieldwork". Also use it when the user shares a .xlsx or .csv file that looks like an XLSForm.

SCTO — SurveyCTO / XLSForm Design Skill

1. INTERPRET THE REQUEST

Before generating any output, identify:

  1. What is being asked?

    • New form from scratch → use standard modules from references/dev-econ-modules.md
    • Add a module or section to an existing form → produce only that section
    • Fix or debug an existing form → read the problem, identify the issue, produce a corrected snippet
    • Explain or document a form → describe the logic in plain language
  2. Survey type (if not stated, ask or assume household):

    • Household / panel / tracking survey
    • Agricultural (plot + crop rosters)
    • Health / nutrition / anthropometrics
    • Firm / enterprise
    • CATI / phone survey
    • Back-check / audit form
  3. What output is needed?

    • Default: produce a .xlsx file via Python/openpyxl AND show the sheet content inline
    • If user says "just show me the table" or "paste the XLSForm": sheet content only
    • If user says "give me the xlsx": Python script + file only
  4. Languages: Default to English only. If user mentions a second language, add label::[Language (code)] and hint::[Language (code)] columns.

  5. Features used: Unless user says otherwise, assume:

    • Mobile data collection (Android SurveyCTO Collect), not web forms
    • Standard constraint and relevancy logic
    • Special values: -88 (Don't know), -89 (Refused), -90 (Not applicable)
    • Variable names Stata-compatible (max 32 characters)

2. STEP-BY-STEP WORKFLOW

For a new form:

  1. Identify modules needed from the request
  2. Look up relevant modules in references/dev-econ-modules.md — use them as the base; adapt labels, choice lists, and field names to the survey context
  3. Select field types using the guide in Section 4 below and references/field-types.md
  4. Assign variable names using the naming convention rules in Section 5
  5. Write skip logic for all conditional fields using patterns in Section 6
  6. Write constraints for all numeric and text fields using patterns in Section 7
  7. Add calculate fields for totals, IDs, module timing, treatment assignment
  8. Assemble the 3-sheet XLSForm (survey, choices, settings)
  9. Run the quality checklist in Section 12 before delivering
  10. Produce the output in the format requested (see Section 9)

For adding a section or module:

  1. State where in the form the section belongs (module prefix, group name)
  2. Produce only the new rows — do not repeat the full form
  3. Note any dependencies (fields referenced in relevant= or calculation= that must exist in the form)

For debugging:

  1. Identify the symptom: field not showing, constraint not firing, calculation returning blank, repeat not working, pulldata returning empty
  2. Check against the debugging patterns in references/repeats-and-rosters.md (repeat debugging checklist) and references/expressions-and-logic.md (syntax rules)
  3. Produce the corrected rows with an explanation of the fix

3. SHEET STRUCTURE CHEATSHEET

Survey sheet — minimum columns (always include):

type | name | label | hint | required | relevant | constraint | constraint message | appearance | calculation | repeat_count | choice_filter

Survey sheet — full column order:

type | name | label | label::[lang] | hint | hint::[lang] | required | required message | relevant | constraint | constraint message | appearance | default | calculation | repeat_count | read only | choice_filter | accuracy_threshold | media:image | media:audio | publishable | minimum_seconds | disabled

Choices sheet — minimum columns:

list_name | name | label

Choices sheet — full column order:

list_name | name | label | label::[lang] | image | filter | [additional filter columns]

Settings sheet — columns:

form_title | form_id | version | default_language | instance_name | public_key | submission_url

Standard settings row example:

form_title: Household Survey Round 1
form_id: ke_hh_survey_r1
version: 2026041301
default_language: english
instance_name: concat(${district_code}, '_', ${ea_code}, '_', ${hh_serial})

4. FIELD TYPE SELECTION GUIDE

Data to collect Field type Notes
Number, whole, ≤9 digits integer Use for age, counts, codes
Long ID / phone (>9 digits) text + appearance: numbers integer overflows at 9 digits
Number with decimals decimal Plot area, prices, ratios
Free text answer text Open-ended questions
Single-choice answer select_one listname Most categorical questions
Multiple-choice answer select_multiple listname Shocks, crops grown, assets
Date date Birthday, event dates
Time time Interview start/end times
Date + time dateTime Use now() in calculation
GPS point geopoint HH location, plot center
Hidden calculated value calculate IDs, totals, flags, lookups
Module time snapshot calculate_here Only for timing — freezes at that point
Display-only text note Instructions, section headers
Photo image Use max-pixels=1024 parameter
Voice recording (question) audio Use quality=voice-only parameter
Survey quality monitoring audio audit See advanced-features.md
Data quality timing text audit Records field-level timing
Interviewer identity enumerator Linked to server dataset
Case ID (case management) caseid Only for case management forms

Critical rules:

  • Never use integer for phone numbers, national IDs, or any number >999,999,999
  • Use calculate_here only for module elapsed-time snapshots; never for skip logic
  • Use once() around any random() or now() in calculate fields
  • geopoint fields: always set accuracy_threshold: 10 (meters)

5. NAMING CONVENTION RULES

Variable names (survey sheet name column)

  • Max 32 characters (Stata compatibility — hard rule)
  • Format: [module_prefix][question_number]_[short_snake_case]
  • Examples: m5_age, h47_spend_food, ag3_plot_area, r1_randnum
  • Start with letter or underscore; only letters, digits, underscores
  • No hyphens, no spaces, no camelCase

Module prefixes

Prefix Module
o Observation / form header / identification
m Household roster / member-level
h Housing / dwelling / assets
f or fc Food consumption / HDDS
nf Non-food expenditure
ag Agriculture (plots, crops, inputs)
lv Livestock
inc Income / livelihoods
sh Shocks / coping strategies
hl Health
an Anthropometrics
we Women's empowerment
r Randomization fields
bc Back-check fields
q Enumerator quality / close-out
t Timing fields
cal_ Calculated helper values
new_ New-round parallel repeat variants (panel surveys)
grp_ Group names (use triggering field: grp_o8, grp_m14)

Choice list names

  • Reusable across modules: yesno, yesnona, yesnodk, relationship, gender, edu_level
  • Module-scoped (single question): m7, h54, ag12
  • Cascading filter: name by the entity type — region, district, ward, village

Special value codes (always include in hints and constraints)

  • -88 = Don't know
  • -89 = Refused / Decline to state
  • -90 = Not applicable
  • 99 = Other (specify)
  • 88 = Don't know (for select_one — use positive integer for choice value)

6. SKIP LOGIC (relevant=) PATTERNS

Show if previous select_one equals a value

${consent} = 1
${m2_gender} = 2
${marital_status} = 1

Show for multiple possible values (or)

${marital_status} = 2 or ${marital_status} = 3 or ${marital_status} = 4
${reason_left} = 3 or ${reason_left} = 4

Show if select_multiple includes a value

selected(${shocks_exp}, '3')
selected(${crops_grown}, '5')
not(selected(${services_used}, '99'))

Show if numeric answer in range

${m5_age} >= 18 and ${m5_age} <= 49
${num_plots} > 0
${hl1_sick} = 1 and ${hl2_sought_care} = 1

Show for "other specify" follow-up

${reason_code} = 99
selected(${multi_field}, '99')

Consent gate — wrap entire module

begin group | grp_main | | relevant=${o9_consent} = 1
  ... all questions ...
end group   | grp_main | |

Show inside repeat for first instance only

position(..) = 1

Show if preloaded value exists

${preloaded_status} = 1
number(${preloaded_count}) > 0
${pulled_value} != ''

Compound conditions

${m2_gender} = 2 and ${m5_age} > 10 and ${m5_age} < 70
${ag0_farms} = 1 and ${ag1_num_plots} > 0

7. CONSTRAINT (constraint=) PATTERNS

Numeric range with special values (house style — use this for all integer/decimal fields)

(.>=0 and .<=120) or .=-88 or .=-89 or .=-90
(.>=0 and .<=100) or .=-88 or .=-89 or .=-90
(.>=0 and .<=30) or .=-88 or .=-89 or .=-90
(.>=0 and .<=1000000) or .=-88 or .=-89 or .=-90

Always include constraint message:

constraint message: Enter a value between 0 and 120, or -88 (Don't know), -89 (Refused), -90 (Not applicable)

Cross-field consistency check

. >= ${daughter_age}
. <= ${total_land_area}
. > 0 and . <= ${hh_size}

String length (text fields)

string-length(.) = 10       # phone number
string-length(.) >= 2 and string-length(.) <= 50

Date constraints

. >= date('2000-01-01') and . <= today()
. >= ${start_date}

GPS accuracy (on geopoint fields, use accuracy_threshold column instead)

accuracy_threshold: 10

Count constraint for select_multiple

count-selected(.) >= 1
count-selected(.) >= 1 and count-selected(.) <= 5

8. CALCULATE FIELD PATTERNS

Composite household / submission ID

concat(${district_code}, '_', ${ea_code}, '_', ${hh_serial})
concat(${region_code}, ${district_code}, ${village_code}, '_', ${hh_num})

Interview duration in minutes

(decimal-time(${endtime}) - decimal-time(${starttime})) * 1440

Module elapsed time (use calculate_here)

type: calculate_here  name: time_ag_end  calculation: once(duration())

Age from birthdate

int((decimal-date-time(today()) - decimal-date-time(${birthdate})) div 365.25)

Frozen datetime stamp

once(format-date-time(now(), '%Y-%b-%e %H:%M:%S'))

Random assignment (always frozen with once())

once(random())
if(${r1_draw} <= 0.5, 1, 0)
if(${r1_draw} < 0.33, 1, if(${r1_draw} < 0.66, 2, 3))

Sum across repeat

sum(${loan_amount})
sum(${ag_plot_area})

Pull from preloaded CSV

pulldata('household_list', 'respondent_name', 'hhid_key', ${hhid})
number(pulldata('baseline', 'hh_size', 'hhid_key', ${hhid}))

Dynamic column name in pulldata (panel survey pattern)

concat('member', index())   # → member1, member2, member3...
pulldata('baseline_hh', ${calc_pos}, 'hhid_key', ${hhid})

Extract repeat value to flat calculate (always do after roster)

indexed-repeat(${m1_name}, ${hh_rg}, 1)
indexed-repeat(${m1_name}, ${hh_rg}, 2)
...
indexed-repeat(${m1_name}, ${hh_rg}, 30)

HDDS or food diversity score

if(${fc1_cereals_yn}=1,1,0) + if(${fc2_roots_yn}=1,1,0) + ... + if(${fc12_misc_yn}=1,1,0)

9. ROSTER METHOD SELECTION GUIDE

Situation Method repeat_count
Count known before entering roster (most common) Method 2 ${hh_size}
Count not known; enumerator enumerates spontaneously Method 1 (blank — + button)
Panel survey: members from prior round preloaded Method 3 ${prior_member_count} (from pulldata)
Nested: plot → crops per plot Nested Method 2 Outer: ${num_plots}; Inner: ${num_crops}
Follow-up questions per selected multi-select item Parallel fixed fields with relevant=selected(${field}, 'value') (no repeat)

When to use indexed-repeat() vs ${fieldname}:

  • Inside the repeat: just ${fieldname} — refers to current instance
  • Outside the repeat: indexed-repeat(${fieldname}, ${repeat_group}, N) — refers to Nth instance
  • Never use indexed-repeat() inside the same repeat it targets

Globally unique names rule:

All field names must be unique across the entire form, not just within the repeat. Use prefixes:

  • Outer repeat: pl_ for plot fields
  • Inner repeat: cr_ for crop fields

10. MULTI-LANGUAGE HANDLING

When to add language columns:

  • User specifies a second language, OR
  • Form is for a multilingual field context

Column naming format:

label::Swahili (sw)
hint::Swahili (sw)
label::Hindi (hi)
hint::Hindi (hi)

Match this format exactly — language name then ISO code in parentheses.

Settings sheet:

default_language: english

Or default_language: English (en) if that matches your label column name exactly.

Choices sheet:

list_name | name | label | label::Swahili (sw)
yesno     | 1    | Yes   | Ndiyo
yesno     | 0    | No    | Hapana

What NOT to translate:

  • name column values (stay English)
  • calculation expressions
  • relevant= and constraint= expressions
  • hint instructions marked "Instruction:" (translate the content, not the prefix)

11. REFERENCE FILE POINTERS

Topic File
Every field type with syntax and appearance values references/field-types.md
Every XLSForm column for survey/choices/settings sheets references/xlsform-columns.md
All operators, functions, relevant/constraint/calculate patterns references/expressions-and-logic.md
Repeat methods, nested repeats, indexed-repeat(), debugging references/repeats-and-rosters.md
Cascading selects, pulldata, server datasets, plug-ins, randomization, encryption, CATI references/advanced-features.md
17 complete copy-paste module blocks (household, ag, health, etc.) references/dev-econ-modules.md

12. PRE-UPLOAD QUALITY CHECKLIST

Run through this before delivering any form output:

Names

  • All name values are ≤32 characters
  • All name values are globally unique (not just unique within group/repeat)
  • begin group and end group names match exactly
  • begin repeat and end repeat names match exactly
  • No spaces, hyphens, or punctuation in variable names (underscores only)

Types and structure

  • Every select_one and select_multiple references a list that exists in the choices sheet
  • Every begin group/begin repeat has a matching end group/end repeat
  • repeat_count field is integer type with required: yes
  • calculate fields have a calculation value; visible fields do not use calculation unless for dynamic default

Logic

  • All relevant= expressions use ${fieldname} (not bare names)
  • All constraint= expressions use . for current field value
  • once() wraps every random(), now(), and duration() in calculate fields
  • selected() always passes choice value as string in single quotes: selected(${field}, '1')
  • indexed-repeat() second argument is the name of the begin repeat row
  • indexed-repeat() index is guarded with relevant=${count} >= N for each N used

Repeats

  • All field names inside repeats are globally unique (not just repeat-local)
  • Inner repeat's repeat_count references a field in the current outer instance
  • No calculate_here inside repeat groups

Choices

  • Every choice list referenced in type column exists in choices sheet
  • Choice name values are integers where possible (Stata compatibility)
  • Cascading selects: filter column in choices matches choice_filter expression column name

Settings

  • form_id has no spaces
  • instance_name evaluates to a string (uses concat(), string() for dates)
  • version follows YYYYMMDDNN format

Output (.xlsx)

  • Three sheets in order: survey → choices → settings
  • Row 1 headers bold and frozen (freeze top row)
  • No merged cells
  • All values as plain strings (no formula cells)
  • Columns auto-sized for readability

13. OUTPUT FORMAT INSTRUCTIONS

Default output: .xlsx file via Python + inline table

Step 1 — Show the sheet content inline as pipe-separated tables:

SURVEY SHEET:
type | name | label | hint | required | relevant | constraint | constraint message | appearance | calculation | repeat_count

CHOICES SHEET:
list_name | name | label

SETTINGS SHEET:
form_title | form_id | version | default_language | instance_name

Step 2 — Produce Python/openpyxl script that creates the .xlsx file:

import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter

wb = openpyxl.Workbook()

# Sheet 1: survey
ws_survey = wb.active
ws_survey.title = 'survey'

# Sheet 2: choices
ws_choices = wb.create_sheet('choices')

# Sheet 3: settings
ws_settings = wb.create_sheet('settings')

# Headers — bold and frozen
SURVEY_HEADERS = ['type', 'name', 'label', 'hint', 'required', 'relevant',
                  'constraint', 'constraint message', 'appearance', 'calculation',
                  'repeat_count', 'choice_filter']

CHOICES_HEADERS = ['list_name', 'name', 'label']

SETTINGS_HEADERS = ['form_title', 'form_id', 'version', 'default_language', 'instance_name']

header_font = Font(bold=True)

def write_headers(ws, headers):
    for col, h in enumerate(headers, 1):
        cell = ws.cell(row=1, column=col, value=h)
        cell.font = header_font
    ws.freeze_panes = ws['A2']

write_headers(ws_survey, SURVEY_HEADERS)
write_headers(ws_choices, CHOICES_HEADERS)
write_headers(ws_settings, SETTINGS_HEADERS)

# --- Survey rows ---
survey_rows = [
    # [type, name, label, hint, required, relevant, constraint, constraint_msg, appearance, calculation, repeat_count, choice_filter]
    # INSERT ROWS HERE
]

for r, row in enumerate(survey_rows, 2):
    for c, val in enumerate(row, 1):
        ws_survey.cell(row=r, column=c, value=val)

# --- Choices rows ---
choices_rows = [
    # [list_name, name, label]
    # INSERT ROWS HERE
]

for r, row in enumerate(choices_rows, 2):
    for c, val in enumerate(row, 1):
        ws_choices.cell(row=r, column=c, value=val)

# --- Settings rows ---
settings_rows = [
    # [form_title, form_id, version, default_language, instance_name]
    # INSERT ROW HERE
]

for r, row in enumerate(settings_rows, 2):
    for c, val in enumerate(row, 1):
        ws_settings.cell(row=r, column=c, value=val)

# Auto-size columns
for ws in [ws_survey, ws_choices, ws_settings]:
    for col in ws.columns:
        max_len = max((len(str(cell.value or '')) for cell in col), default=0)
        ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 4, 60)

wb.save('form_output.xlsx')
print('Saved: form_output.xlsx')

Step 3 — Run the script to confirm it executes without error, then report the output filename.

Inline table output only (when user requests "just show the table"):

Produce pipe-separated tables for each sheet. Include all non-empty columns. Omit columns that are entirely blank for the current form.

Python script only (when user requests "give me the xlsx"):

Produce the full self-contained script with all rows populated. Include a print() confirmation at the end.