Avery
RuntimeUse casesPricingHelpBlog
← All postsBlog

How to write a Change Request that AI actually executes well

2026-06-12 · Avery NXR

Your AI coding tool returns mediocre code. The output looks plausible. It compiles. The tests you wrote (if you wrote any) pass. Then production breaks and you find the edge case the AI didn't handle.

The problem isn't the AI. The AI did what you asked. The problem is your prompt wasn't a spec.

This post is the difference. What a Change Request (CR) actually is, the five components every good CR has, the common mistakes that produce mediocre code, and three real examples showing how to write CRs that ship.

Whether you're using Avery NXR, Cursor, Claude Code, or any other AI coding tool, the spec quality determines the code quality. Better specs are the new senior engineer skill.

What a Change Request actually is

A Change Request is not a prompt. It's not a Jira ticket. It's a complete specification for one unit of shippable work.

The contents of a CR:

Context. The relevant state of the system today. What exists, what doesn't, what assumptions the AI should make.

Requirements. What needs to change. What the new behavior should be.

Acceptance criteria. How to verify the change is correct. The concrete tests of done.

Edge cases. The failure modes and unusual scenarios that need handling.

Test plan. The tests that prove the implementation works.

A complete CR is half a page to two pages of structured specification. It's longer than a prompt. It's also executable in a way prompts aren't.

The reason: prompts assume the AI knows things. Specs eliminate the assumptions.

Why prompts produce mediocre code

A prompt like "Add user authentication" produces code that:

Handles the happy path of signup and login.

Doesn't handle duplicate emails (the AI makes a choice; it's often wrong).

Doesn't rate limit (the AI doesn't add it without being asked).

Doesn't validate password strength (the AI defaults to whatever the framework defaults to).

Doesn't integrate with your existing user model (the AI invents a new one).

The prompt didn't say to do any of those things. The AI did exactly what was asked. The result is code that "works" in a narrow sense and breaks in production.

The fix isn't a smarter AI. The fix is a better spec. The spec encodes the constraints the AI couldn't have known.

The five components of a good CR

1. Context

What the system does today. What relevant code exists. What patterns the codebase uses. What assumptions to maintain.

Bad: "We have a Next.js app."

Good: "This is a Next.js 15 app using Prisma with Postgres. We have a User model with fields id, email, createdAt, but no auth implementation. We use NextAuth.js elsewhere for partner login but not for end users. Our session management goes through Postgres-backed sessions."

The good version saves the AI from making wrong assumptions. The wrong assumptions are where bugs come from.

2. Requirements

What needs to change. What the new behavior should be. Specific enough that any engineer reading it could implement it the same way.

Bad: "Add authentication."

Good: "Add email and password authentication to the existing Next.js app. Users sign up with email and password. Login with the same. Sessions persist for 30 days via Postgres-backed sessions. Password reset via email link with 1-hour expiry."

The good version states exactly what behavior is expected.

3. Acceptance criteria

How to verify the implementation is correct. The concrete tests of done.

Bad: "Make sure it works."

Good: "Acceptance: (1) New user can sign up with valid email and password. (2) Existing user can log in with correct credentials. (3) Wrong password produces a generic 'invalid credentials' error (not 'wrong password'). (4) Logout clears the session. (5) Password reset email arrives within 30 seconds and the link works for exactly 1 hour. (6) All flows work in incognito and in regular browser."

The good version is testable. Either each criterion is met or it isn't. No interpretation needed.

4. Edge cases

The failure modes and unusual scenarios. The places where mediocre implementations fall down.

Bad: (nothing specified, AI defaults to happy path)

Good: "Edge cases: (1) Signup with already-registered email returns 409 with specific error 'email already registered'. (2) Login attempts rate-limited to 5 per IP per hour, returning 429 when exceeded. (3) Password reset attempts rate-limited to 3 per email per hour. (4) Password reset link used twice returns 410 with 'link already used'. (5) Email service failure during signup keeps the user record in unverified state, retries via background job."

The good version anticipates failure modes the happy-path tests would miss.

5. Test plan

The tests the implementation must include. Both what to test and at what level.

Bad: "Write some tests."

Good: "Tests: (1) Unit tests for password hashing function. (2) Integration tests for signup, login, logout, password reset flows. (3) Integration tests for each edge case above. (4) End-to-end test that signs up a user, logs them out, logs them back in, requests a password reset, and confirms the new password works. Use Vitest for unit/integration, Playwright for E2E."

The good version specifies the testing infrastructure and the test cases. The AI writes the tests as part of the implementation.

Three real CR examples

Example 1: Simple feature

Context: This is a Next.js 15 app with Prisma and Postgres. We have a User model with id, email, createdAt, lastLoginAt. We don't currently track user activity beyond lastLoginAt.

Requirements: Add an ActivityLog model that tracks user actions. Each entry: user_id (FK), action_type (enum: login, logout, password_change, profile_update, api_call), action_metadata (JSON), ip_address, user_agent, timestamp. Add a helper function logActivity(userId, actionType, metadata, request) that creates a log entry. Wire it into the existing login, logout, and password change endpoints.

Acceptance criteria:

  • ActivityLog migration runs cleanly.
  • Login endpoint creates a log entry with action_type=login.
  • Logout endpoint creates a log entry with action_type=logout.
  • Password change creates a log entry with action_type=password_change.
  • IP address and user agent are captured from the request.
  • Existing tests still pass.

Edge cases:

  • If logActivity fails (database issue), the main action still succeeds. Activity logging is not a blocker.
  • IP address handles X-Forwarded-For correctly when behind a proxy.
  • User agent is truncated to 500 chars if longer.

Test plan:

  • Unit tests for logActivity function with mocked Prisma.
  • Integration test for login flow that asserts a log entry is created.
  • Integration test for the X-Forwarded-For handling.

This CR is small enough to ship in a day. The AI executes it well because every requirement is explicit.

Example 2: Complex feature

Context: This is a Next.js 15 multi-tenant SaaS app. Each Organization has many Users. Currently, all users in an Organization see the same data. We need to add role-based access control.

Requirements: Add a Role model (id, organization_id, name, permissions) where permissions is a JSON array of permission strings like "billing.view", "billing.edit", "users.invite". Add UserRole join table (user_id, role_id). Add a hasPermission(userId, permission) function that checks all the user's roles. Update the existing /admin/billing route to require "billing.view" and the /admin/billing/edit route to require "billing.edit". Seed three default roles per organization on org creation: Admin (all permissions), Member (view only), Billing (billing.view + billing.edit only).

Acceptance criteria:

  • Role and UserRole migrations run cleanly.
  • New organizations get the three default roles automatically.
  • The org admin (first user) gets the Admin role automatically.
  • hasPermission(userId, permission) returns true if any of the user's roles has the permission.
  • /admin/billing returns 403 for users without billing.view.
  • /admin/billing/edit returns 403 for users without billing.edit.
  • Admin role inherits all permissions.

Edge cases:

  • User with no roles (race condition during signup) returns false for all permissions, gets a clear error message.
  • Permission check on a user that doesn't exist returns false (not error).
  • Multiple roles with overlapping permissions: the union of permissions applies.
  • Permissions check is case-sensitive ("billing.view" != "Billing.View"). Normalize at write time.
  • Inactive users (status=deactivated) return false for all permissions regardless of roles.

Test plan:

  • Unit tests for hasPermission with various role configurations.
  • Integration tests for the protected routes.
  • Migration tests that verify the default roles are created.
  • End-to-end test that creates an organization, verifies the default roles, attempts protected actions, and confirms the access control.

This CR is more complex. It would ship in 2-3 days. The AI handles it well because the complexity is in the spec, not in implementation guessing.

Example 3: Refactor

Context: Our OrderService.createOrder function has grown to 300 lines. It handles input validation, inventory checking, payment processing, order creation, fulfillment trigger, customer notification, and analytics logging. It's hard to test in isolation and has had three bugs in the past month due to coupling.

Requirements: Refactor createOrder into discrete steps using a step-based pattern. Each step is a class implementing IOrderStep. The orchestrator runs the steps in order. The steps: ValidateInputStep, CheckInventoryStep, ProcessPaymentStep, CreateOrderRecordStep, TriggerFulfillmentStep, NotifyCustomerStep, LogAnalyticsStep. If any step fails, run defined rollback actions for prior steps. Maintain backward compatibility: external callers still call OrderService.createOrder; the public interface doesn't change.

Acceptance criteria:

  • Each step is its own file, its own class, its own tests.
  • The orchestrator runs steps in order and stops on failure.
  • Failure triggers documented rollback actions.
  • Existing OrderService.createOrder callers work unchanged.
  • All existing tests pass.
  • Each step has new unit tests with mocked dependencies.

Edge cases:

  • Payment succeeds but order record creation fails: refund the payment and surface the failure.
  • Order record created but fulfillment trigger fails: order is in 'pending_fulfillment' state, fulfillment retries via background job.
  • Notification fails: order stands, notification queued for retry.
  • Analytics logging fails: order stands, analytics is fire-and-forget.

Test plan:

  • Unit tests for each step in isolation.
  • Integration test for the happy path.
  • Integration tests for each failure scenario (each step failing).
  • Performance test: refactored version should be within 5% of original latency.

Refactors are where AI coding tools earn their keep most. This CR is complex but well-specified. The AI does the work; the human reviews the structure.

The five common CR mistakes

Mistake 1: too vague

"Add a feature for user feedback" is not a CR. It's a project. The AI guesses and the guess is usually wrong.

Fix: scope down. "Add a thumbs up/down rating on the dashboard tile that posts to /api/feedback" is a CR.

Mistake 2: too many features

"Add authentication, user profiles, and password reset" is three CRs. The AI does the first reasonably and the others poorly because attention degrades across a long spec.

Fix: split into separate CRs. Ship them in sequence.

Mistake 3: missing context

"Add OAuth" assumes the AI knows your existing auth, your framework, your session model. The AI guesses wrong.

Fix: spell out the context. Three sentences about the existing state saves hours of revision.

Mistake 4: missing edge cases

"Handle errors appropriately" is not a spec. The AI handles errors by catching exceptions and logging them, which is rarely what you want.

Fix: enumerate the failure modes you care about. Specify the desired behavior for each.

Mistake 5: no test plan

"Tests should be added" produces minimal happy-path tests. The AI does the bare minimum.

Fix: specify what tests must exist and at what level. The AI writes them.

How to break a feature into CRs

The single biggest CR-writing skill is decomposition. Most features are 3-10 CRs, not 1.

The decomposition pattern:

Start with the user outcome. "User can authenticate via Google OAuth."

Identify the system components that need to change. Database schema. Auth flow. Login UI. Logout flow. Session management.

For each component, scope a CR. Each CR should be shippable in a day or two. Each should pass tests independently.

Sequence the CRs. Dependencies determine order. Database schema first. Auth flow next. UI on top.

Write each CR with the components from this post. Context, requirements, acceptance criteria, edge cases, test plan.

For our OAuth example, you'd end up with:

CR 1: Add Provider model to support multiple OAuth providers.

CR 2: Implement Google OAuth flow (auth callback, token exchange).

CR 3: Update User model to link to Provider tokens.

CR 4: Update login UI to add Google login button.

CR 5: Handle the edge cases (Google account email matches existing User, etc.).

Five CRs, sequenced, each shippable. The AI executes each one well because each is well-specified.

When the CR is too big

If you can't fit the spec in a page, the CR is too big. Decompose.

If the AI's output requires major revision after the first attempt, the CR was probably underspecified. Update the CR with what you learned and try again.

If the same CR has produced three buggy implementations across iterations, the CR is probably trying to do something the AI doesn't know how to do. Either you need to provide more context, or you need to do this one yourself.

These are all signals. Trust them.

The senior engineer skill

In the AI coding era, the senior engineer's job is increasingly about specs, not code.

Junior engineers write good code from a spec.

AI coding agents write good code from a spec.

Senior engineers write good specs.

The skills that produce good specs: understanding the system architecture, anticipating edge cases, knowing the integration points, being precise about acceptance criteria, decomposing complex changes into shippable units.

These were always the underlying senior engineer skills. They were partially hidden by the daily work of writing code. AI tools surface them.

The engineers who develop the CR-writing skill in 2026 become the highest-leverage members of any team. The ones who don't become as valuable as the AI replaces.

The closing thought

A great CR feels obvious when you write it. The structure is the structure. Every component has its place. The AI executes well.

A bad CR feels like writing a request and hoping. The AI guesses. The output is mediocre.

The difference between great and bad CRs is whether the human did the thinking the AI can't.

The thinking is the work. The code is the output. AI tools made the code cheap. The thinking is now the bottleneck.

If your CRs follow the structure above, your AI coding tool produces production code. If they don't, you'll keep getting prototypes that need rewriting.

avery.software