Step-by-Step Instructions
Learning Outcomes
- Break complex coding tasks into numbered steps that AI can follow sequentially
- Order steps correctly based on dependencies and logical flow
- Judge when step-by-step helps versus when it over-constrains
- Handle sequential dependencies between steps
- Combine narrative descriptions with numbered instructions for optimal results
Lesson Plan
| Segment | Duration | Topic |
|---|---|---|
| Intro | 3 min | Why sequence matters for complex tasks |
| Explain | 7 min | Breaking tasks into numbered steps |
| Demo | 8 min | Ordering for optimal results |
| Explain | 7 min | When steps help vs constrain |
| Demo | 7 min | Handling sequential dependencies |
| Practice | 6 min | Combining narrative and numbered approaches |
| Wrap-up | 2 min | Key takeaways, preview next lesson |
Before You Begin
Pre-work:
- Complete Lessons 1-2 (Anatomy and Role/Context)
- Think of a recent multi-file or multi-step coding task
- Review: have you ever given AI a complex task and got back something that was "close but wrong"?
Shopping List:
- Any AI coding tool (Claude Code, Cursor, or Codex CLI)
- A project with a feature you'd like to build that requires 3+ steps
- Your prompts folder for saving experiments
When a task involves multiple operations, AI performs significantly better when you break it into explicit numbered steps. Without steps, AI must infer the correct order — and it often gets it wrong.
The problem with monolithic prompts:
Build a user registration feature with email verification, password hashing,
database storage, and a welcome email.
This might produce a single giant function that mixes all concerns, or it might miss one of the four requirements entirely, or implement them in an order that doesn't work (like sending the email before storing the user).
The step-by-step version:
Implement user registration. Follow these steps in order:
1. Create a registerUser function that accepts { email, password, name }
2. Validate the input (email format, password minimum 8 chars, name non-empty)
3. Check if a user with this email already exists — if so, throw ConflictError
4. Hash the password using bcrypt with 12 rounds
5. Store the user record in the database with is_verified = false
6. Generate a verification token (random 32-byte hex string) with 24h expiry
7. Store the token in a verification_tokens table linked to the user
8. Send a verification email using the sendEmail service
9. Return { userId, message: "Verification email sent" } — never return the token
Each step should be its own clearly separated block of code with a comment.
Why steps work:
- Completeness — each requirement is explicit, nothing gets skipped
- Order — dependencies are handled (you can't hash before validating)
- Testability — each step is a unit that can be verified
- Clarity — AI knows exactly what "done" means
How many steps is right?
- 3-5 steps: Most common, good for feature implementations
- 6-10 steps: Complex workflows, multi-stage processes
- 10+ steps: Consider splitting into multiple prompts instead
The order of your steps matters more than you might think. AI generates code sequentially — each step builds on what came before. Getting the order wrong creates cascading problems.
Rule 1: Dependencies first
If step B uses something created in step A, A must come first.
Wrong order:
1. Write the route handler that calls userService.getProfile()
2. Create the UserService class with a getProfile method
3. Define the User interface
Right order:
1. Define the User interface with the fields we need
2. Create the UserService class with a getProfile method that returns User
3. Write the route handler that calls userService.getProfile()
Rule 2: Data structures before logic
Define types, interfaces, and schemas before the code that uses them.
1. Define the OrderStatus enum (pending, confirmed, shipped, delivered, cancelled)
2. Define the Order interface with all fields including status: OrderStatus
3. Create the state machine rules (which transitions are valid)
4. Implement the transitionOrder function that enforces the rules
5. Add the API endpoint that calls transitionOrder
Rule 3: Simple before complex
Start with the straightforward path, then add complexity.
1. Implement the basic search — query the database by title, return results
2. Add pagination (page, limit parameters, total count in response)
3. Add filtering (by category, date range, price range)
4. Add sorting (by relevance, date, price — ascending/descending)
5. Add caching — cache results for identical queries for 5 minutes
Rule 4: Setup before action
Configuration, initialisation, and connections come before the code that uses them.
1. Set up the Redis connection with retry logic
2. Create the cache utility (get, set, invalidate methods)
3. Implement the product service with caching at the repository layer
4. Add cache invalidation hooks on product update/delete
Step-by-step instructions are powerful but not always appropriate. Knowing when to use them — and when NOT to — is a key skill.
Steps HELP when:
- The task has a clear sequence (build A, then B uses A)
- You've tried a freeform prompt and got incomplete or disordered results
- The task involves multiple files or components
- There are critical ordering constraints (migrate data before deleting old columns)
- You need to ensure nothing is skipped
Steps CONSTRAIN when:
- You're exploring possibilities ("What's the best approach to...?")
- The task is genuinely simple and unambiguous
- You want creative solutions (steps force a specific path)
- You're over-specifying implementation details
Example: When steps hurt
Over-specified:
Create a utility function to format currency:
1. Accept a number as input
2. Create a variable called formatter
3. Use Intl.NumberFormat with locale "en-US"
4. Set style to "currency" and currency to "USD"
5. Call formatter.format(number)
6. Return the result
This is so detailed that you've essentially written the code yourself in English. Better:
Create a currency formatting utility that:
- Supports multiple currencies (USD, EUR, GBP at minimum)
- Accepts locale as a parameter with "en-US" as default
- Handles edge cases: NaN, negative numbers, very large numbers
- Returns a string like "$1,234.56"
The second version tells AI WHAT you want without dictating every line of HOW. This leaves room for AI to apply best practices you might not have thought of.
The guideline: Use steps for ORCHESTRATION (what happens in what order) but not for IMPLEMENTATION (how each individual piece works internally).
Good use of steps (orchestration):
1. Validate the incoming webhook payload
2. Determine which event type it is
3. Route to the appropriate handler
4. Execute the handler and capture the result
5. Return appropriate HTTP status code
Bad use of steps (micro-managing implementation):
1. Create a variable called payload
2. Parse the JSON body
3. Check if payload.type exists
4. If it's "order.created" call handleOrderCreated
5. If it's "order.updated" call handleOrderUpdated
...
Some tasks have steps where the output of one step becomes the input to the next. Managing these dependencies clearly prevents AI from making incorrect assumptions.
Explicit dependency notation:
1. Create the database migration to add a "preferences" JSONB column
to the users table with a default value of {}
2. Update the User model to include the preferences field
(use the column from step 1)
3. Create a PreferencesService that provides get/set/merge operations
on the user's preferences (works with the model from step 2)
4. Add PUT /users/me/preferences endpoint that accepts partial updates
(uses PreferencesService from step 3)
5. Add validation middleware that ensures preference keys are from
an allowed list: ["theme", "language", "notifications", "timezone"]
(attach to the endpoint from step 4)
Notice how each step references which previous step it builds on. This prevents AI from creating disconnected code.
Handling branching dependencies:
Sometimes steps don't form a linear chain. Indicate this:
Steps 1-2 are independent (can be in either order):
1. Create the EmailTemplate interface { subject, body, variables }
2. Create the NotificationChannel enum { email, sms, push }
Steps 3-4 depend on both 1 and 2:
3. Create the NotificationService that accepts a channel and template
4. Implement the send method with channel-specific logic
Step 5 depends on 3-4:
5. Create the API endpoint that triggers notifications
The "carry forward" technique:
When steps build on each other, you can tell AI to maintain state:
Implement a data pipeline with these sequential stages:
1. FETCH: Download JSON from the API endpoint, handle rate limiting
Output: raw JSON array
2. TRANSFORM: Map the raw data to our internal format
Input: raw JSON array from step 1
Output: array of CleanRecord objects
3. VALIDATE: Check each record for required fields and valid ranges
Input: CleanRecord array from step 2
Output: { valid: CleanRecord[], invalid: ErrorRecord[] }
4. LOAD: Batch insert valid records into PostgreSQL
Input: valid records from step 3
Also: log invalid records for review
Each function should accept the output of the previous step as input.
Use TypeScript generics to enforce the pipeline type safety.
The most effective prompts for complex tasks often combine a narrative overview with numbered steps. The narrative provides the "why" and big picture; the steps provide the "what" in sequence.
The hybrid pattern:
I need to add real-time notifications to our app. Currently, users
only see new messages when they refresh the page. We want them to
receive instant updates via WebSocket when they get a new message,
a friend request, or a system alert.
Our stack: Node.js backend, React frontend, PostgreSQL.
We already have a messages table and a REST endpoint for fetching messages.
Implementation steps:
1. Set up a WebSocket server alongside our existing Express app
(use the ws library, share the same HTTP server)
2. Create an authentication handshake — clients send their JWT
during the WebSocket upgrade, reject unauthenticated connections
3. Maintain a Map of userId -> WebSocket connections (handle multiple
tabs/devices per user)
4. Create a notify(userId, event) utility that looks up connections
and sends JSON messages
5. Hook into existing services: when a message is saved to DB,
call notify(recipientId, { type: "new_message", data: message })
6. On the frontend, create a useWebSocket hook that:
- Connects on mount, authenticates with stored JWT
- Reconnects with exponential backoff on disconnect
- Exposes incoming events via a callback
- Cleans up on unmount
7. Integrate the hook into the NotificationBell component to show
real-time unread count
Keep the WebSocket server stateless (no in-memory message queues) —
if we scale to multiple servers later, we'll add Redis pub/sub.
Why this works: The narrative (first paragraph) explains the motivation, current state, and goal. The technical context (second paragraph) sets constraints. The steps provide clear implementation order. The final note adds a forward-looking constraint.
Another example — the "context then steps" pattern:
Context:
Our e-commerce checkout currently processes payments synchronously.
This causes timeouts when the payment provider is slow (>5s).
We need to move to an async pattern where the user sees "processing"
and we confirm via webhook.
Current flow: Cart -> POST /checkout -> charge card -> show result
Target flow: Cart -> POST /checkout -> return "processing" ->
receive webhook -> update order -> notify user
Steps to implement:
1. Add an "order_status" column to orders (pending, processing,
confirmed, failed) — currently we only have confirmed orders
2. Modify POST /checkout to:
- Create order with status "processing"
- Send payment request to provider with our webhook URL
- Return 202 Accepted with { orderId, status: "processing" }
3. Create POST /webhooks/payment to receive provider callbacks:
- Verify the webhook signature
- Update order status based on payment result
- Trigger appropriate follow-up (email confirmation or failure notice)
4. Add GET /orders/:id/status for the frontend to poll
(simple endpoint returning { status, updatedAt })
5. Update the frontend checkout page to:
- Show a "processing" state after submit
- Poll /orders/:id/status every 2 seconds
- Transition to success/failure page when status changes
- Timeout after 30 seconds with "we'll email you" message
When to use pure narrative (no steps):
- Design discussions ("What architecture would you recommend for...?")
- Code review ("Review this function for potential issues")
- Explanation requests ("Explain how this middleware chain works")
When to use pure steps (minimal narrative):
- Well-understood tasks with clear technical requirements
- Migrations and upgrades ("Upgrade from React Router 5 to 6")
- Sequences where the context is already established
When to combine both:
- Feature implementations (most common)
- System design + implementation
- Refactoring with a clear goal and multiple stages
Questions & Answers
Key Takeaways
- Numbered steps prevent AI from skipping requirements — each step is an explicit checkpoint.
- Order matters: dependencies first, data structures before logic, simple before complex.
- Use steps for orchestration, not implementation — describe outcomes, not line-by-line code.
- Name your step outputs — "Output: array of CleanRecord" prevents shape mismatches between steps.
- Combine narrative (the why) with steps (the what) — this gives AI both motivation and a clear path.
Next Steps: In Lesson 4 — Few-Shot Examples, we'll learn how showing AI what you want (with input/output examples) can be more powerful than describing it.