Discover recipe

Pre and Post Survey: Measuring Program Impact in a Community

Measure whether an event, course or campaign actually changed anything. Two identical survey waves, scored the same way, give you a before and after you can compare per member.

Audience
community researchers, developers or educators running structured programs on Discord, moderators tracking the effect of specific initiatives
Time to set up
~20 minutes per wave (identical questions; second project is a clone with the dates changed)
Best for
any community running a program where you want evidence of impact, not just anecdote

A two-wave survey design that measures change in knowledge or attitudes across your community before and after an intervention: an event, a content series, a campaign, or a moderation change. Each wave is a separate Subo project with identical questions; joining the exports on Discord handle gives you individual-level change scores.


The scenario

You run a game-development Discord with 1,500 members. You’re launching a 4-week “Indie Dev Bootcamp”, with weekly talks, live critique sessions, shared build exercises. You want to know whether it actually changed participants’ confidence and knowledge, and whether the effect differs between members who showed up to most sessions versus those who attended once or twice.

You run:

You join the two exports on Discord handle and compute individual delta scores. The result is a participant-level dataset showing who improved, by how much, and in which dimensions.


Design principles

Same questions, same scoring, different projects

Both waves use identical question blocks, identical scale points and identical calculated-field names. Using separate projects (not retakes of a single project) is deliberate:

Clone Wave 1’s project to create Wave 2, so all blocks, scale points, calculated fields and bucket names copy exactly. Change the project name and intro text; leave everything else identical.

Use scored attitudes, not just counts

A single “How confident are you?” rating question gives you one number but conflates dimensions. Scoring across multiple items per dimension, even 3-4 items, produces a more stable and interpretable construct score.

Example: measuring “confidence in shipping” across three items:

Averaged, those give a Shipping score on a 1.0 to 4.0 range. That is more diagnostic than a single rating, and it stays on a scale a reader already understands.

Closed questions, not free text, for pre/post

Use opinion_scale for attitude items and single_punch for knowledge items. Free text answers cannot be scored automatically; they require manual coding and break the join logic. Save open-ended questions for items that are not part of the scored constructs.

Include a response-matching anchor

The Discord handle is the natural join key: it is consistent across both waves and appears in the Responses export. No extra question needed. But if any respondents might use different Discord accounts, add a free-text “nickname” question whose answer can serve as a secondary key.


Flow (both waves identical on scored items)

  ┌──────────────────────────────────────────┐
  │ q1: Intro (content_block)                │
  │ Wave 1: "Quick check-in before we start" │
  │ Wave 2: "How did the bootcamp go?"       │
  └──────────────────────────────────────────┘


  ┌──────────────────────────────────────────┐
  │ q2–q5: Dimension 1 items (4 questions)   │
  │ "Confidence in shipping" (opinion_scale) │
  │ averaged → [Shipping], 1.0-4.0           │
  └──────────────────────────────────────────┘


  ┌──────────────────────────────────────────┐
  │ q6–q9: Dimension 2 items (4 questions)   │
  │ "Confidence in scoping" (opinion_scale)  │
  │ averaged → [Scoping], 1.0-4.0            │
  └──────────────────────────────────────────┘


  ┌──────────────────────────────────────────┐
  │ q10–q12: Knowledge items (3 questions)   │
  │ Right/wrong, with correct_answer_index   │
  │ score_values → [score_knowledge]         │
  │ Immediate feedback in Wave 2 only        │
  └──────────────────────────────────────────┘

                    ▼  (Wave 2 only)
  ┌──────────────────────────────────────────┐
  │ q13: Attendance question (Wave 2 only)   │
  │ "How many sessions did you attend?"      │
  │ (plain single_punch, no score_values)    │
  └──────────────────────────────────────────┘


  ┌──────────────────────────────────────────┐
  │ q14: Summary content_block               │
  │ Shows [Shipping], [Scoping] and          │
  │ [correct_answers]/[max_correct_answers]  │
  └──────────────────────────────────────────┘

Prerequisites

Constructs (3 total, same names in both projects)

Two kinds, because attitude items and knowledge items score differently now:

Display nameHow it is builtRangeMeaning
Shippingcalculated_block averaging the opinion_scale items1.0-4.0Confidence in shipping decisions
Scopingcalculated_block averaging the opinion_scale items1.0-4.0Confidence in scoping and prioritizing
Knowledgescore bucket knowledge[score_knowledge]0-NFactual knowledge of the topic domain

Knowledge items keep score_values, which is what score buckets are for: one option is right and carries the points. Attitude items do not, because the scale already stores a number.

Projects (2 total)

// Wave 1
POST /v1/communities/{communityId}/projects
{
  "name": "Bootcamp — Pre-test (Wave 1)",
  "scoring_enabled": true,
  "settings": { "max_completes_per_user": 1, "is_anonymous": false }
}

// Wave 2 — clone Wave 1, then update name and intro block
POST /v1/communities/{communityId}/projects/{wave1Id}/clone
// → update name to "Bootcamp — Post-test (Wave 2)"
// → update q1 intro text to post-program framing
// → add attendance question (q13) at the end of scored items

Important: verify that the cloned Wave 2 project has identical block names, identical calculated-field names and identical bucket names before publishing. A mismatch breaks comparability: Wave 1 [Shipping] and Wave 2 [Shipping_confidence] are different columns in the export, and a scale whose points were re-typed in a different order is worse, because it lines up silently and averages the wrong way.


Scored item design

Attitude items (opinion_scale, one word on every point)

Use an opinion_scale with a label on every point. That is a Likert item, and the stored answer is the point number, so it averages and compares across waves with nothing configured. Avoid 5-point scales with a midpoint: midpoints attract satisficing responses and add noise in small community samples. A 4-point scale has no middle to hide in.

{
  "type": "opinion_scale",
  "prompt": "I have a clear picture of what I would cut to hit a deadline.",
  "min": 1, "max": 4,
  "options": [
    { "value": "1", "label": "Strongly disagree", "emoji": "😡" },
    { "value": "2", "label": "Disagree",          "emoji": "🙁" },
    { "value": "3", "label": "Agree",             "emoji": "🙂" },
    { "value": "4", "label": "Strongly agree",    "emoji": "😍" }
  ]
}

Two things to notice, because they are where this differs from a choice question. option.value is the point number as a string, not the words: the words go in label, and the number is what gets stored. And a scale carries no score_values at all, because it does not need them. The construct score comes from a calculated field instead, which is the next section.

Before August 2026 this recipe built the same item as a single_punch with score_values 1 to 4 on the options. That was the right workaround when Subo had no scale types, but it stores an option id rather than a number, so nothing downstream can average it. If you have surveys built the old way, they keep working; build new ones with opinion_scale.

The direction rule, and it bites hardest here. A labelled set must run low to high. The stored answer is the point number, so a set with “Strongly agree” at 1 silently inverts every average and every >= 3 condition built on it. That also means reverse-framed items cannot just be dropped in: if you write a negatively worded statement to guard against acquiescence bias, it needs reverse-scoring in the calculated field (5 - [Item] on a 1-4 scale), not a reversed label set. For most Discord communities, consistently positive framing is less confusing and avoids the problem entirely.

Group 3-4 items per dimension.

Turning the items into a construct score

A scale has no score bucket, so a dimension’s score is a calculated_block over the items by name:

{
  "type": "calculated_block",
  "name": "Shipping",
  "formula": "([Ship1] + [Ship2] + [Ship3]) / 3"
}

That is a mean on the scale’s own 1.0 to 4.0 range, which is easier to read across waves than a bucket total: a wave-one 2.3 against a wave-two 3.1 needs no explaining, where “9 out of 12” does.

Two constraints worth knowing before you build it:

Knowledge items (right/wrong with deferred feedback)

For knowledge items, set correct_answer_index and score_values on the correct option only, the same pattern as the World Capitals Quiz.

In Wave 1, leave when_correct and when_incorrect blank: you do not want to give feedback that helps respondents perform better on Wave 2 knowledge items. In Wave 2, you can add feedback text freely because the pre-test has already been collected.

// Wave 1 — no feedback (don't tip off respondents before post-test)
{
  "type": "single_punch",
  "prompt": "What is the recommended maximum scope for a solo-developer launch milestone?",
  "options": [
    { "value": "1 feature", "label": "1 core feature + polish" },
    { "value": "3 features", "label": "3 features with basic UX", "score_values": { "knowledge": 5 } },
    { "value": "MVP+",       "label": "MVP + growth loop" },
    { "value": "Full launch","label": "Full feature set" }
  ]
}

// Wave 2 — same question, now with feedback
{
  "type": "single_punch",
  "prompt": "What is the recommended maximum scope for a solo-developer launch milestone?",
  "when_correct":   "✅ Correct — 3 focused features with basic UX is the sweet spot for a solo launch. Less is almost always more.",
  "when_incorrect": "❌ The answer is **3 features with basic UX**. The most common mistake is launching too wide; scope creep after commitment is what kills solo projects.",
  "options": [
    { "value": "1 feature", "label": "1 core feature + polish" },
    { "value": "3 features", "label": "3 features with basic UX", "score_values": { "knowledge": 5 } },
    { "value": "MVP+",       "label": "MVP + growth loop" },
    { "value": "Full launch","label": "Full feature set" }
  ]
}

Closing block: show the score to the respondent

In Wave 2, show the respondent their score. This closes the loop and rewards honesty. In Wave 1, you may choose to hide it (to avoid anchoring the respondent to their pre-test score):

// Wave 2 closing block
{
  "type": "content_block",
  "prompt": "# Your bootcamp results, [UserName]\n\n**Shipping confidence:** [score_shipping] / [max_score_shipping]\n**Scoping confidence:** [score_scoping] / [max_score_scoping]\n**Knowledge check:** [correct_answers] / [max_correct_answers] correct\n\nThanks for taking part — your data helps us make the next cohort better.",
  "continue": { "after": "click", "label": "Done ✓" }
}

Analysis: computing change scores

Export both waves

From the Responses tab → Export → CSV. The export includes:

Join and compute deltas

In any spreadsheet or analysis tool:

wave2.score_shipping - wave1.score_shipping  → Δ_shipping
wave2.score_scoping  - wave1.score_scoping   → Δ_scoping
wave2.score_knowledge - wave1.score_knowledge → Δ_knowledge

Members present in Wave 2 but not Wave 1 are late joiners or non-completers on the pre-test, so analyze them separately or exclude depending on your goal.

Segment by attendance (Wave 2 q13)

Group respondents by their Wave 2 attendance answer (1 session / 2-3 / all 4). Compare mean Δ_shipping, Δ_scoping, Δ_knowledge across attendance groups. A dose-response pattern (higher attendance → larger positive delta) is the strongest available evidence that the program caused the improvement.

Report aggregate change

Average the delta columns across all matched respondents for a program-level summary: “Participants improved their shipping confidence score by +2.4 points on average (out of a possible 16), with the largest gains among those who attended 3 or more sessions.”


Variations

Three-wave design (pre / mid / post)

Add a Wave 1.5 survey at the midpoint of the program. Same structure, same block, field and bucket names. A mid-program dip in confidence is common: members learn how much they don’t know, and the pre/mid/post trajectory tells a richer story than a single before/after comparison.

Segment change by onboarding type

If you’ve run the Member Segmentation survey, join the segmentation export to the pre/post export on Discord handle. Do Competitors improve their shipping confidence more than Socials? Do Creators enter with higher baseline scoping scores? Cross-segment change analysis adds a third axis to the report.

Continuous measurement (monthly pulse)

Drop the “pre/post” framing and run the same scored survey monthly as a community health pulse. Track community-level averages over time rather than individual deltas. Useful for ongoing programs rather than discrete cohorts.


Limitations and honest notes

These limitations are inherent to naturalistic community research, not specific to Subo. The platform gives you clean, structured data collection and automatic score computation. Interpreting that data still requires judgement.


When this recipe doesn’t fit


Frequently asked questions

How do you measure the impact of a community program?

Run the same survey twice, once before the program and once after, with identical questions and identical scoring. The change in each score is your evidence, rather than anecdote.

How do you match responses between the two waves?

The Discord handle is the natural join key because it is consistent across both waves and requires no extra question.

Why score several questions per dimension?

A single item conflates dimensions and is noisy. Three or four opinion_scale items averaged into one construct score produce a more stable measure you can actually compare across waves.

Should the rating scale have a midpoint?

Avoid one. Midpoints attract satisficing responses and add noise without adding information about direction. An opinion_scale with an even number of points has no middle to hide in.

Do I have to rebuild the survey for wave two?

No. Clone wave one and change the dates. All blocks, scale points, calculated fields and bucket names carry over, which is what keeps the two waves comparable.

Build it

Every recipe here is a Subo script. Author it in the web app, or send the payload straight to the public API. New communities start free.

Add Subo to your server Browse templates