DEV Community

Cover image for I Built Surveys That Get Smarter With Every Response
Adam Pitera
Adam Pitera

Posted on

I Built Surveys That Get Smarter With Every Response

Using Google Gemini to generate follow-up questions based on what each person says — and what's missing from the dataset.

I wrote about the concept behind Dyadem when I built it for the Gemini 3 Hackathon. Since then I've iterated on it — added an AI question authoring step, deployed it for a real community questionnaire, and learned a lot about what works. This article goes into the engineering.

Most surveys ask everyone the same questions. You get shallow data and bored respondents.

I built Dyadem — an open-source survey platform where AI generates follow-up questions for each respondent based on two things: what they just said, and what gaps exist across all responses so far. Early respondents get broad exploratory questions. By response 50, the AI is asking about specific things nobody's mentioned yet.

Here's how it works and what I learned.

The Flow

User answers 3 fixed questions
  → App gathers dataset context (aggregate stats, themes, gaps)
  → Builds a prompt: this person's answers + what everyone else said
  → Gemini generates 1-2 follow-up questions
  → User answers them
  → Everything submitted together
Enter fullscreen mode Exit fullscreen mode

Stack: Next.js 14.2, TypeScript, PostgreSQL (Neon), Drizzle ORM, Google Gemini.

The Prompt That Makes It Work

This is the bit I found most interesting to design. The prompt gives Gemini two types of context: what the individual said, and a statistical picture of the whole dataset.

You are designing follow-up questions for an anonymous survey.

This person just answered:
- Biggest financial pressure: {{biggest_pressure}}
- How things have changed (1=much better, 5=much worse): {{change_direction}}
- What they've sacrificed: "{{sacrifice}}"

Current dataset ({{total_responses}} responses so far):
- Top pressure: {{top_pressure}} ({{top_pressure_pct}}%)
- Average change score: {{avg_change}}/5
- Most common sacrifice themes: {{sacrifice_themes}}
{{emerging_gap_line}}

Generate 1-2 follow-up questions that:
1. DIG DEEPER into this person's specific situation
2. FILL GAPS — ask about something the community hasn't covered yet
3. Are QUICK to answer (choice, scale, or short text)
4. Feel CONVERSATIONAL, not clinical
5. NEVER ask for identifying information

{{volume_guidance}}
Enter fullscreen mode Exit fullscreen mode

The {{volume_guidance}} bit shifts the AI's strategy based on how much data exists:

if (context.totalResponses < 10) {
  volumeGuidance = "We have very few responses. Ask broader questions "
    + "to establish baseline understanding.";
} else if (context.totalResponses < 50) {
  volumeGuidance = "We're building a picture. Start probing for "
    + "nuance within the dominant themes.";
} else {
  volumeGuidance = "We have substantial data. Ask targeted questions "
    + "to uncover the most surprising or underreported patterns.";
}
Enter fullscreen mode Exit fullscreen mode

This means the survey changes character as it collects data. The first few people get "tell us about you" questions. Later respondents get "nobody's mentioned X yet — what's your experience?" It's a small amount of logic that makes a big difference to data quality.

Getting Structured Output From an LLM

The other problem worth solving: Gemini returns text, but I need a question object I can render in React. Zod + Gemini's structured output mode handles this.

Define what a valid question looks like:

export const adaptiveQuestionSchema = z.object({
  questions: z.array(
    z.object({
      question_text: z.string(),
      input_type: z.enum(["single_choice", "scale", "short_text"]),
      options: z.array(z.string()).optional(),
      scale_min_label: z.string().optional(),
      scale_max_label: z.string().optional(),
      reasoning: z.string(),
    })
  ).min(1).max(2),
});
Enter fullscreen mode Exit fullscreen mode

Pass that schema directly to Gemini:

const response = await getAI().models.generateContent({
  model: MODELS.flash,
  contents: prompt,
  config: {
    responseMimeType: "application/json",
    responseJsonSchema: zodToJsonSchema(adaptiveQuestionSchema),
  },
});
Enter fullscreen mode Exit fullscreen mode

No parsing. No regex. No "please return JSON" in the prompt and hoping for the best. The model is constrained at the API level to return exactly this shape.

The reasoning field is worth calling out — it forces the model to explain why it chose each question. This works like chain-of-thought prompting but baked into the output schema. It improves question quality and gives you something to debug with.

Cost: Cheaper Than You'd Think

I used three different Gemini models matched to the task:

Task Model Cost per call
Content moderation Flash Lite ~$0.0005
Adaptive questions Flash ~$0.001–0.003
Theme extraction & reports Pro ~$0.01–0.03

Total per survey submission: about $0.002–0.004. Cheap enough for a community project with no budget.

If the AI Breaks, the Survey Still Works

One design rule I'd push for any AI-enhanced feature: the AI should improve the experience, never block it.

try {
  const res = await fetch(`/api/s/${slug}/adaptive-questions`, {
    method: "POST",
    body: JSON.stringify(answers),
  });
  if (res.ok) {
    const data = await res.json();
    if (data.questions?.length > 0) {
      setAdaptiveQuestions(data);
      setStage("adaptive");
      return;
    }
  }
} catch {
  // AI failed — skip to submission
}
doSubmit(answers, null);
Enter fullscreen mode Exit fullscreen mode

If Gemini is down or slow, people still submit their survey. You lose the follow-ups, not the whole product.

Next Steps

Response quality scoring. Use the AI to score how substantive each free-text answer is. Vague responses get a gentler follow-up encouraging more detail. Detailed answers get acknowledged and move on. Adaptive effort, not just adaptive questions.

Live gap visualisation. A real-time dashboard showing survey creators which topics are well-covered and which have gaps as responses come in — with the ability to manually nudge the AI's priorities. "Stop asking about rent. Start asking about childcare."

Respondent clustering. After enough responses, identify respondent archetypes and tailor the follow-up strategy per cluster, not just per individual answer.

Research-grade export. Structured export that preserves the adaptive question context — not just a CSV. A researcher needs to know why each person got different follow-ups to interpret the data properly.

Takeaways

Give the AI more than just the current input. The follow-up questions are good because Gemini sees the individual's answers and the aggregate dataset. Without that broader context, you just get generic follow-ups.

Constrain output with schemas, not prompts. Asking an LLM to "please return JSON" is fragile. Gemini's responseJsonSchema with Zod validation is reliable.

Volume-aware prompting matters. Shifting the AI's behaviour based on how much data exists is a small detail that changes the character of the whole system.


Dyadem is open source — GitHub repo here if you want to look at the full implementation.

Top comments (0)