04 Checklists 6 chapters

Theme 06 · Agent-Aware

Checklists

Explanation

Checklists

Plain Human Explanation

Checklists turn repeated risky work into visible steps. They are not busywork. They are a way to keep releases, migrations, incident fixes, and agent handoffs from depending on memory.

For users, a good checklist means fewer missed verification steps. A payment fix should include a billing-state check. A privacy change should include a copy and data-flow check. A migration should include rollback thinking.

Technical Explanation

Use short, local checklists for workflows where the same verification matters every time. The checklist should name the product risk, required evidence, and stop conditions.

In code, this often means modeling required checks as data instead of scattered comments. The important part is not automation by itself. The important part is making the expected verification hard to skip silently.

Why It Matters

  • User impact: fewer releases ship with missed safety checks.
  • Product behavior: repeated workflows follow the same verification standard.
  • Risk: agents and humans both tend to complete the visible task and miss the quiet follow-up step.
  • Decision point: add a checklist when the work is repeated, risky, and easy to partially complete.

The Core Move

Write the verification path next to the workflow, keep it short, and make every required step produce concrete evidence.

Small Example

Checklists: Small Example

Bad TypeScript Example

export function canShipRelease(options: { testsPassed?: boolean; reviewed?: boolean }) {
  return options.testsPassed || options.reviewed;
}
export function canShipRelease(options: {
  testsPassed?: boolean;
  reviewed?: boolean;
}) {
  return (
    options.testsPassed || options.reviewed
  );
}

Good TypeScript Example

type ReleaseCheck = "tests-passed" | "review-complete" | "rollback-noted";

type CompletedReleaseCheck = {
  check: ReleaseCheck;
  evidence: string;
};

const requiredReleaseChecks: ReleaseCheck[] = [
  "tests-passed",
  "review-complete",
  "rollback-noted",
];

export function findMissingReleaseChecks(completed: CompletedReleaseCheck[]): ReleaseCheck[] {
  const completedNames = new Set(completed.map(item => item.check));
  return requiredReleaseChecks.filter(check => !completedNames.has(check));
}
type ReleaseCheck =
  | "tests-passed"
  | "review-complete"
  | "rollback-noted";

type CompletedReleaseCheck = {
  check: ReleaseCheck;
  evidence: string;
};

const requiredReleaseChecks: ReleaseCheck[] =
  [
    "tests-passed",
    "review-complete",
    "rollback-noted",
  ];

export function findMissingReleaseChecks(
  completed: CompletedReleaseCheck[],
): ReleaseCheck[] {
  const completedNames = new Set(
    completed.map((item) => item.check),
  );

  return requiredReleaseChecks.filter(
    (check) => !completedNames.has(check),
  );
}

What Changed

  • The bad version lets one passed flag hide missing release work.
  • The good version names every required check explicitly.
  • Each completed check carries evidence, so “done” means more than a checked box.

Realistic Example

Checklists: Realistic Example

A migration can look complete after the SQL runs, while the product is still unsafe because nobody checked counts, backfill gaps, or rollback notes.

Bad TypeScript Example

export async function runMigration(db: any) {
  await db.exec("alter table subscriptions add column canceled_at text");
  await db.exec("update subscriptions set canceled_at = ended_at where status = 'canceled'");

  return { ok: true };
}
export async function runMigration(
  db: any,
) {
  await db.exec(
    "alter table subscriptions add column canceled_at text",
  );

  await db.exec(
    "update subscriptions set canceled_at = ended_at where status = 'canceled'",
  );

  return {
    ok: true,
  };
}

Good TypeScript Example

type MigrationEvidence = {
  rowCountBefore: number;
  rowCountAfter: number;
  rollbackPlan: string;
};

type MigrationCheckResult =
  | { ok: true }
  | { ok: false; error: "row-count-changed" | "rollback-plan-missing" };

export function verifySubscriptionMigration(evidence: MigrationEvidence): MigrationCheckResult {
  if (evidence.rowCountBefore !== evidence.rowCountAfter) {
    return { ok: false, error: "row-count-changed" };
  }

  if (evidence.rollbackPlan.trim().length === 0) {
    return { ok: false, error: "rollback-plan-missing" };
  }

  return { ok: true };
}
type MigrationEvidence = {
  rowCountBefore: number;
  rowCountAfter: number;
  rollbackPlan: string;
};

type MigrationCheckResult =
  | {
      ok: true;
    }
  | {
      ok: false;
      error:
        | "row-count-changed"
        | "rollback-plan-missing";
    };

export function verifySubscriptionMigration(
  evidence: MigrationEvidence,
): MigrationCheckResult {
  if (
    evidence.rowCountBefore !==
    evidence.rowCountAfter
  ) {
    return {
      ok: false,
      error: "row-count-changed",
    };
  }

  if (
    evidence.rollbackPlan.trim().length ===
    0
  ) {
    return {
      ok: false,
      error: "rollback-plan-missing",
    };
  }

  return {
    ok: true,
  };
}

What Changed

  • The bad version treats “SQL ran” as the whole migration outcome.
  • The good version names the verification evidence a migration needs before handoff.
  • The checklist is small enough to live near the migration and specific enough to catch missed safety work.

System Example

Checklists: System Example

At system scale, checklists help releases combine code changes with product verification. They work best when they are tied to the workflow being shipped, not stored as a generic wall of rules.

Larger System-Level Bad TypeScript Example

type DeployRequest = {
  service: string;
  skipChecks?: boolean;
};

export async function deploy(request: DeployRequest, shell: any) {
  if (!request.skipChecks) {
    await shell.run("npm test");
  }

  await shell.run(`deploy ${request.service}`);
  return { deployed: true };
}
type DeployRequest = {
  service: string;
  skipChecks?: boolean;
};

export async function deploy(
  request: DeployRequest,
  shell: any,
) {
  if (!request.skipChecks) {
    await shell.run("npm test");
  }

  await shell.run(
    `deploy ${request.service}`,
  );

  return {
    deployed: true,
  };
}

Larger System-Level Good TypeScript Example

type DeployCheckName = "lint" | "typecheck" | "tests" | "release-notes";

type DeployEvidence = {
  check: DeployCheckName;
  passed: boolean;
  outputPath: string;
};

type DeployPlan = {
  service: "api" | "worker" | "web";
  requiredChecks: DeployCheckName[];
};

type DeployDecision =
  | { ok: true; service: DeployPlan["service"] }
  | { ok: false; missing: DeployCheckName[]; failed: DeployCheckName[] };

export function decideDeploy(plan: DeployPlan, evidence: DeployEvidence[]): DeployDecision {
  const evidenceByCheck = new Map(evidence.map(item => [item.check, item]));
  const missing = plan.requiredChecks.filter(check => !evidenceByCheck.has(check));
  const failed = plan.requiredChecks.filter(check => evidenceByCheck.get(check)?.passed === false);

  if (missing.length > 0 || failed.length > 0) {
    return { ok: false, missing, failed };
  }

  return { ok: true, service: plan.service };
}
type DeployCheckName =
  | "lint"
  | "typecheck"
  | "tests"
  | "release-notes";

type DeployEvidence = {
  check: DeployCheckName;
  passed: boolean;
  outputPath: string;
};

type DeployPlan = {
  service: "api" | "worker" | "web";
  requiredChecks: DeployCheckName[];
};

type DeployDecision =
  | {
      ok: true;
      service: DeployPlan["service"];
    }
  | {
      ok: false;
      missing: DeployCheckName[];
      failed: DeployCheckName[];
    };

export function decideDeploy(
  plan: DeployPlan,
  evidence: DeployEvidence[],
): DeployDecision {
  const evidenceByCheck = new Map(
    evidence.map((item) => [
      item.check,
      item,
    ]),
  );

  const missing =
    plan.requiredChecks.filter(
      (check) =>
        !evidenceByCheck.has(check),
    );

  const failed = plan.requiredChecks.filter(
    (check) =>
      evidenceByCheck.get(check)?.passed ===
      false,
  );

  if (
    missing.length > 0 ||
    failed.length > 0
  ) {
    return {
      ok: false,
      missing,
      failed,
    };
  }

  return {
    ok: true,
    service: plan.service,
  };
}

What Changed

  • The bad version treats verification as an optional deploy flag.
  • The good version models the service-specific checks and the evidence for each check.
  • Deployment can be blocked with a clear reason instead of relying on someone to remember which command mattered.

When To Use It

Checklists: When To Use It

Use This When

  • The work is repeated and a skipped step can hurt users or data.
  • The task has evidence requirements: commands run, screenshots checked, counts compared, rollback noted, or support copy reviewed.
  • Agents or humans often finish the code but miss the handoff or verification step.

Avoid This When

  • The checklist would restate obvious habits like “read the code” or “write good tests.”
  • The work is a one-off exploration with no known repeat path yet.
  • The checklist is so long that nobody can tell which steps are required.

Tradeoffs

Checklists add process. Keep them short and tied to real risk. A useful checklist blocks missed work; a vague checklist becomes background noise.

  • Local conventions
  • Explicit interfaces
  • Standards written for coding agents

Practice Prompt

Checklists: Practice Prompt

Beginner Exercise

Pick one recurring task, such as a release, migration, or billing fix. Write three checks that produce concrete evidence.

Intermediate Exercise

Turn that checklist into a small TypeScript type or data structure. Include the check name, whether it passed, and where the evidence lives.

Stretch Exercise

Add a decision function that blocks handoff when required evidence is missing. Keep it local to the workflow instead of creating a global process engine.

Reflection Question

Which user or support problem would happen if someone skipped one checklist item?

Suggest an edit

Leave a private editorial note. This creates a GitHub issue for this curriculum page.