Task Instructions
Overview
Participants need to understand the task before the experiment begins. Poor instructions are one of the most common sources of noise in online behavioral research. This lesson builds a multi-step instructions page with a comprehension check: participants who cannot answer a basic question about the task are sent back to review before they start generating data.
We also record two things about how they did: whether they completed the instructions, and how many attempts the check took them. That second number is a small data-quality signal you get for free. A participant who needed five attempts read the task differently than one who passed on the first.
What You're Building
This is a preview of the page we are building. This one works: click through the steps, take the check, and try answering it wrong on purpose to see what a participant would see.
Step 1 of 3
Welcome to the Study
In this study you will press buttons on screen. Your goal is simply to press the buttons throughout the session.
The real page adds three behaviors on top of what you just clicked through:
- Progress is enforced, not assumed. The check must be answered correctly to continue. Wrong answers highlight in red, and the only way forward is back through the instructions.
- Attempts are recorded. Passing the check saves
instructionsCompleted: trueand the number of attempts to the participant's record, then moves them to the task. - The full page has the same gate as the others. Saving progress requires the participant cookie; without it, back to the start.
Building It With Claude Code
One new thing this lesson: this page is interactive in a way the earlier pages weren't. It has to remember which step the participant is on and update the screen as they click, without reloading. That means part of it runs in the participant's browser (the frontend, from the architecture lesson) while the saving still happens on the server. You don't need to manage that split; describing the behavior is enough, and Claude Code will put the code on the right sides.
@prisma/schema.prisma
Build a multi-step instructions page at src/app/instructions/. It should:
1. Show 3 instruction screens describing a button-press task where
pressing may earn points, with Next and Back buttons and a step
counter.
2. After the last screen, show a 1-question comprehension check asking
what happens when you press the button. On submit, highlight the
correct answer green and any wrong selection red. If the answer is
wrong, show a button to review the instructions and try again.
3. When the answer is correct, update the participant's record with
instructionsCompleted set to true and the number of attempts it took,
then redirect to /task. The save should read the participant cookie
and redirect to / if it is missing.
Add instructionsCompleted (boolean, default false) and
comprehensionAttempts (number, default 0) to the Participant model, run
the migration (name it add-instructions-fields), and regenerate the
Prisma client.
Approve the schema edit and the migration as before. Claude Code will
likely create two files in src/app/instructions/: the page itself and a
small server file for the save. That split is normal; it's the
frontend/backend line made visible.
Testing What You Built
Go through the flow with a new Prolific ID: entry, consent, demographics, and you should land on the instructions.
Click through all three steps, using Back at least once to confirm it works. On the check, deliberately pick a wrong answer and submit. Confirm the red highlight appears and the review button takes you back to step 1.
Go through again and answer correctly. You will hit a 404 at /task, the
last unbuilt page in the flow. Open Prisma Studio with npm run db:studio
and check the participant's row: instructionsCompleted should be true and
comprehensionAttempts should be 2, one failed attempt plus one pass.
If You Get Stuck
Same pattern: what you did, what you expected, what happened, plus any error text. Common symptoms:
- The right answer gets marked wrong. The check is comparing against the wrong option. "On the comprehension check, selecting 'You may earn a point' gets marked incorrect, but it is the right answer. Fix which option the check treats as correct."
- Submit does nothing. "I select an answer and click Submit on the comprehension check and nothing happens. Find out why."
- You pass the check but the record doesn't update. "I passed the comprehension check and was redirected, but instructionsCompleted is still false in Prisma Studio. The completion is not being saved."
- The attempt count is wrong. "comprehensionAttempts shows 0 in Prisma Studio even though I failed once and then passed. It should show 2. Fix the attempt counting."
- You're stuck in a loop between the check and the review. "After failing the check and reviewing the instructions, submitting again does nothing and I can't get past the check. Find out why."
Make It Your Own
The instruction text and the check question need to match your actual study. Describe your task and Claude Code will rewrite both together:
@src/app/instructions/page.tsx
Update the instruction text to describe a button-press task where
participants press the spacebar instead of clicking a button. Change the
comprehension check question to match. Keep the same three-step structure.
If your study needs more than one comprehension question, ask for that too; the check is a list, and adding a question is one sentence of instruction.
The Code, If You're Curious
You don't need to read anything in this section to continue the course. It's here for the curious and for anyone who wants to compare against the checkpoint branch. This lesson has three pieces: two new schema fields, a small server file that records completion, and the interactive page.
The schema: two new fields on Participant
model Participant {
id String @id @default(cuid())
prolificId String @unique
createdAt DateTime @default(now())
consented Boolean @default(false)
demographics Demographics?
instructionsCompleted Boolean @default(false) // true after passing the check
comprehensionAttempts Int @default(0) // how many tries it took
}The server file: recording completion
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import prisma from "@/lib/prisma";
export async function completeInstructions(attempts: number) {
const cookieStore = await cookies();
const participantId = cookieStore.get("participantId")?.value;
if (!participantId) {
redirect("/");
}
await prisma.participant.update({
where: { id: participantId },
data: {
instructionsCompleted: true,
comprehensionAttempts: attempts,
},
});
redirect("/task");
}The page: steps, check, and highlighting
"use client";
import { useState } from "react";
import { completeInstructions } from "./actions";
const steps = [
{
title: "Welcome to the Study",
body: "In this study you will press buttons on screen. Your goal is simply to press the buttons throughout the session.",
},
{
title: "How Points Work",
body: "Pressing a button may earn you a point. Not every press will earn one. Just continue pressing at whatever pace feels natural to you.",
},
{
title: "Duration",
body: "The study takes approximately 20 minutes. Please complete it in one sitting without interruption. Do not close the browser tab or navigate away during the task.",
},
];
const questions = [
{
question: "What happens when you press the button?",
options: [
"You always earn a point",
"You may earn a point",
"Nothing happens",
"The study ends",
],
correct: 1,
},
];
export default function InstructionsPage() {
const [step, setStep] = useState(0);
const [showCheck, setShowCheck] = useState(false);
const [answers, setAnswers] = useState<number[]>(
Array(questions.length).fill(-1)
);
const [submitted, setSubmitted] = useState(false);
const [attempts, setAttempts] = useState(0);
const allCorrect = questions.every((q, i) => answers[i] === q.correct);
async function handleCheckSubmit() {
const nextAttempts = attempts + 1;
setAttempts(nextAttempts);
setSubmitted(true);
if (allCorrect) {
await completeInstructions(nextAttempts);
}
}
if (showCheck) {
return (
<div className="max-w-xl mx-auto px-6 py-12">
<h1 className="text-2xl font-bold text-zinc-900 dark:text-white mb-2">
Comprehension Check
</h1>
<p className="text-zinc-500 dark:text-zinc-400 mb-8">
Please answer the following questions before starting.
</p>
<div className="space-y-8">
{questions.map((q, qi) => (
<div key={qi}>
<p className="font-medium text-zinc-800 dark:text-zinc-200 mb-3">
{q.question}
</p>
<div className="space-y-2">
{q.options.map((opt, oi) => {
const selected = answers[qi] === oi;
const isCorrect = oi === q.correct;
let style =
"w-full text-left px-4 py-3 rounded border text-sm ";
if (!submitted) {
style += selected
? "border-zinc-900 dark:border-white bg-zinc-100 dark:bg-zinc-800 font-medium text-zinc-900 dark:text-white"
: "border-zinc-300 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300";
} else {
if (isCorrect)
style +=
"border-green-500 bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-300";
else if (selected)
style +=
"border-red-400 bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-300";
else
style +=
"border-zinc-200 dark:border-zinc-700 text-zinc-400 dark:text-zinc-600";
}
return (
<button
key={oi}
className={style}
onClick={() => {
if (submitted) return;
const next = [...answers];
next[qi] = oi;
setAnswers(next);
}}
>
{opt}
</button>
);
})}
</div>
</div>
))}
</div>
{submitted && !allCorrect && (
<div className="mt-8">
<p className="text-red-600 dark:text-red-400 text-sm mb-4">
One or more answers were incorrect. Please review the instructions
and try again.
</p>
<button
className="w-full border border-zinc-300 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium py-3 rounded mb-3"
onClick={() => {
setStep(0);
setShowCheck(false);
setAnswers(Array(questions.length).fill(-1));
setSubmitted(false);
}}
>
Review Instructions
</button>
</div>
)}
{!submitted && (
<button
className="w-full mt-8 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 font-semibold py-3 px-6 rounded disabled:opacity-40"
disabled={answers.some((a) => a === -1)}
onClick={handleCheckSubmit}
>
Submit Answers
</button>
)}
</div>
);
}
const current = steps[step];
return (
<div className="max-w-xl mx-auto px-6 py-12">
<p className="text-sm text-zinc-400 dark:text-zinc-500 mb-6">
Step {step + 1} of {steps.length}
</p>
<h1 className="text-2xl font-bold text-zinc-900 dark:text-white mb-4">
{current.title}
</h1>
<p className="text-zinc-600 dark:text-zinc-400 text-lg leading-relaxed mb-12">
{current.body}
</p>
<div className="flex gap-3">
{step > 0 && (
<button
className="flex-1 border border-zinc-300 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 font-medium py-3 rounded"
onClick={() => setStep(step - 1)}
>
Back
</button>
)}
<button
className="flex-1 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 font-semibold py-3 rounded"
onClick={() => {
if (step < steps.length - 1) {
setStep(step + 1);
} else {
setShowCheck(true);
}
}}
>
{step < steps.length - 1 ? "Next" : "Continue to Check"}
</button>
</div>
</div>
);
}The short version: the page runs in the participant's browser and tracks
which step is showing, which answer is selected, and how many attempts have
happened. The instruction text lives in the steps list and the check in
questions, so editing content means editing those lists. When every answer
is correct, the page calls the server file above, which writes the result
and moves the participant to the task.
🌿 Checkpoint branch: 05-instructions-page: This is the finished
instructions page. Check out this branch to see the completed page and catch
up to this point in the course.
⚠️ If you built earlier lessons yourself and then switch to this
branch: your database was set up by your own migrations, not this
branch's, so npm run db:migrate will report a mismatch and offer to
reset the database. Accepting the reset is the right move during the
course: it deletes your practice data and rebuilds the tables to match
this branch. Never accept a reset once real participant data is in the
database.