Build an AI Task Automator with TypeScript

Build a TypeScript CLI tool that sends goals to Claude and returns structured action plans.

Introduction

30 Second Summary

Think about how many times you set a goal and then spent ages figuring out where to start. What if you could type your goal into a terminal and get back a prioritized, time-estimated action plan in seconds?

In this project, you will build a command-line tool with TypeScript that sends any goal to the Claude AI and receives a structured, step-by-step action plan in response.

What You'll Build

You'll create a TypeScript CLI application that integrates with Anthropic's Claude API to generate structured action plans from natural language goals.

By the end of this project, you'll have:

  • A complete TypeScript project with package.json, tsconfig.json, and source files.
  • A typed interface defining the structure of an action plan with priorities and time estimates.
  • A working CLI tool that calls the Claude API with a system prompt and returns formatted action plans for any goal you type in.
  • Secret Mission: Add multi-turn conversation so you can ask follow-up questions about your plan.

Are there any prerequisites?

You'll need an Anthropic API key, Node.js installed on your machine, and a code editor like VS Code or Cursor. Step 1 walks you through setting everything up.

Set Up Your TypeScript Project

Before you can build the AI Task Automator, you need a properly configured TypeScript project. TypeScript adds type safety on top of JavaScript, catching bugs before your code ever runs.

In this step, you'll create your project folder, install the necessary packages, and configure TypeScript so it's ready for development.

In this step, get ready to:
  • Create the project folder and initialize it with npm.
  • Install TypeScript, tsx, and the Anthropic SDK.
  • Configure TypeScript and verify the setup works.
Create your project folder and initialize npm

Every Node.js project starts with a folder and a package.json file that tracks your project's name, dependencies, and configuration.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ai-task-automator
cd ai-task-automator
  • Confirm the folder exists by running:
ls

You should see the ai-task-automator folder listed on your Desktop.

  • Initialize your project by running this command:
npm init -y

What does this command do?

The npm init -y command creates a package.json file with default values. The -y flag means "yes to all defaults" so you skip the interactive prompts.

  • Set your project to use ES modules by running this command:
npm pkg set type=module

What are ES modules?

ES modules are the modern standard for organizing JavaScript and TypeScript code. Setting type to module lets you use import and export syntax instead of the older require() style.

Seeing 'npm: command not found'?

This means Node.js is not available in your current terminal session. Close the terminal and reopen it, then run node --version to confirm Node.js is installed.

help me fix npm command not found

Install TypeScript and dependencies

Your project needs three key packages. The Anthropic SDK connects your code to the Claude API. TypeScript adds type checking. And tsx lets you run TypeScript files directly without a separate compile step.

  • Install the Anthropic SDK by running this command:
npm install @anthropic-ai/sdk
  • Install TypeScript, tsx, and Node.js type definitions as dev dependencies by running this command:
npm install --save-dev tsx typescript @types/node

What's the difference between dependencies and devDependencies?

Regular dependencies (@anthropic-ai/sdk) are packages your code needs to run. Dev dependencies (tsx, typescript, @types/node) are tools you need during development but not in production.

  • Add a project description by running this command:
npm pkg set description="AI-powered task automation CLI built with TypeScript"
  • Add convenience scripts to your project by running these commands:
npm pkg set scripts.start="tsx src/index.ts"
npm pkg set scripts.typecheck="tsc --noEmit"
  • Remove the default fields that npm generated (you don't need them) by running these commands:
npm pkg delete main
npm pkg delete scripts.test
npm pkg delete keywords
npm pkg delete author
npm pkg delete license

What do those scripts do?

The start script lets you run your app with npm start instead of typing the full tsx command every time. The typecheck script runs TypeScript's compiler to check for type errors without producing output files.

Seeing permission errors during install?

If you see EACCES or permission denied errors, make sure you are running the terminal as a regular user (not with sudo). On macOS, you may need to fix npm permissions. On Windows, try running Command Prompt as administrator.

help me fix npm permission errors

✔️ Awesome, I've got everything!

Great. Double check you've saved all your changes and your terminal shows no errors from the install commands.

ⓧ I'd like to double check the full code

Your package.json should look similar to this. Version numbers may differ slightly depending on when you run the install.

{
  "name": "ai-task-automator",
  "version": "1.0.0",
  "description": "AI-powered task automation CLI built with TypeScript",
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "@types/node": "^22.15.0",
    "tsx": "^4.22.4",
    "typescript": "^5.8.0"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.102.0"
  }
}
Create and configure tsconfig.json

TypeScript needs a configuration file that tells it how to check your code. This file controls things like which JavaScript version to target and where your source files live.

  • Generate a TypeScript configuration file by running this command:
npx tsc --init

You should see a message saying Successfully created a tsconfig.json file in your terminal.

What is tsconfig.json?

The tsconfig.json file tells the TypeScript compiler how to behave. It controls which files to check, what JavaScript features to target, and how strict to be about type checking.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code (or Cursor if you use Cursor) and press Enter to open your editor.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the ai-task-automator folder, and click Open.

You should see package.json, tsconfig.json, and a node_modules folder in the file explorer on the left.

  • Click on tsconfig.json in the file explorer to open it.
  • Select all the text in the file with Cmd+A (macOS) or Ctrl+A (Windows).
  • Delete it and replace the entire contents with this configuration:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "isolatedModules": true
  },
  "include": ["src/**/*"]
}

What do these settings mean?

  • target and module tell TypeScript to use the latest JavaScript features and Node.js module system.
  • rootDir points to your source folder (./src) where your TypeScript files live.
  • noEmit means TypeScript only checks types without producing JavaScript files. The tsx tool handles running your code directly.
  • strict enables the strictest type checking, which catches more bugs early.
  • Save tsconfig.json with Cmd+S (macOS) or Ctrl+S (Windows).

Now create your first TypeScript file to verify everything works.

  • Create a src folder inside your project. In the terminal at the bottom of your editor, run:
mkdir src
  • Create a new file called index.ts inside the src folder. In your editor's file explorer, right-click the src folder and select New File.
  • Name the file index.ts and press Enter.
  • Add this code to the file:
console.log("Hello TypeScript!");
  • Save src/index.ts with Cmd+S (macOS) or Ctrl+S (Windows).
  • Verify everything works by running this command in your terminal:
npx tsx src/index.ts

You should see Hello TypeScript! printed in your terminal. That confirms TypeScript, tsx, and your project structure are all working together.

Don't see 'Hello TypeScript!' in the output?

Make sure src/index.ts is saved. Check that the file is inside the src folder, not in the project root.

If you see a module-related error, confirm your package.json has "type": "module" and that tsconfig.json has "module": "nodenext".

help me debug my TypeScript setup

✔️ Awesome, I've got everything!

Great. Double check all your files are saved in the editor.

ⓧ I'd like to double check the full code

Your tsconfig.json should look like this:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "isolatedModules": true
  },
  "include": ["src/**/*"]
}

Your src/index.ts should look like this:

console.log("Hello TypeScript!");

Your TypeScript environment is ready to go. Next up, you'll define the types and interfaces that give your AI Task Automator its structure.

Define Your Types and Interfaces

Your TypeScript project is set up and running. Now it's time to give your code some structure.

TypeScript's real power comes from its type system. By defining interfaces, you describe the exact shape of your data before writing any logic. This means your editor can catch mistakes instantly, and your code becomes self-documenting.

In this step, get ready to:
  • Define TypeScript interfaces that describe the shape of an action plan.
  • Build a helper function that formats plan data into readable terminal output.
  • Test your types by running a hardcoded sample through the formatter.
Create the types file

Interfaces act like blueprints for your data. They tell TypeScript exactly which properties an object must have and what type each property should be.

  • Create a new file called src/types.ts by running this command in your terminal:
touch src/types.ts
  • Open src/types.ts in your editor and add the following code:
export interface ActionStep {
  stepNumber: number;
  action: string;
  timeEstimate: string;
  priority: "high" | "medium" | "low";
}

export interface ActionPlan {
  goal: string;
  steps: ActionStep[];
  totalTime: string;
  summary: string;
}

What does this code do?

You've defined two interfaces that describe the structure of an action plan:

  • ActionStep describes a single step in a plan. The priority field uses a union type ("high" | "medium" | "low") so only those three values are allowed.
  • ActionPlan describes the full plan. The steps field is typed as an array of ActionStep objects, linking the two interfaces together.
  • Save src/types.ts.

Seeing red squiggly lines in your editor?

Check that each property ends with a semicolon, not a comma. Interfaces use semicolons to separate properties.

Also make sure you have the export keyword before each interface. Without it, other files won't be able to import these types.

Why are my interfaces not exporting correctly?

Create a helper function with type annotations

Now that you have the interfaces, you can write a function that uses them. Type annotations on the function's parameter and return value tell TypeScript exactly what goes in and what comes out.

  • Create a new file called src/helpers.ts by running:
touch src/helpers.ts
  • Open src/helpers.ts and add the following code:
import type { ActionPlan } from "./types.js";

export function formatPlan(plan: ActionPlan): string {
  let output = `\n Goal: ${plan.goal}\n`;
  output += ` Total Time: ${plan.totalTime}\n`;
  output += `─────────────────────────────────\n`;

  for (const step of plan.steps) {
    const priorityLabel =
      step.priority === "high" ? "[!]" :
      step.priority === "medium" ? "[~]" : "[ ]";

    output += `  ${priorityLabel} Step ${step.stepNumber}: ${step.action}\n`;
    output += `      Time: ${step.timeEstimate}\n\n`;
  }

  output += `─────────────────────────────────\n`;
  output += ` Summary: ${plan.summary}\n`;

  return output;
}

What does this code do?

The formatPlan function takes an ActionPlan and returns a string. TypeScript enforces both of these annotations at compile time.

  • import type imports only the type information, not any runtime code. This is a TypeScript best practice for type-only imports.
  • The import path uses ./types.js (not .ts). This is required when using ES modules with TypeScript. TypeScript resolves .js imports to .ts files automatically.
  • The priorityLabel ternary assigns a visual icon based on each step's priority level.
  • Save src/helpers.ts.

Seeing an error on the import line?

Make sure you wrote ./types.js and not ./types.ts or ./types. With ES module resolution, TypeScript requires the .js extension in import paths.

Why does TypeScript use .js in import paths for .ts files?

Test your types work

The best way to confirm your interfaces and function work together is to use them. You'll replace the Hello TypeScript! line in index.ts with a hardcoded sample plan that exercises the full type system.

  • Open src/index.ts in your editor.
  • Select all the existing code and replace it with the following:
import type { ActionPlan } from "./types.js";
import { formatPlan } from "./helpers.js";

const samplePlan: ActionPlan = {
  goal: "Learn TypeScript",
  steps: [
    { stepNumber: 1, action: "Read the TypeScript handbook", timeEstimate: "2 hours", priority: "high" },
    { stepNumber: 2, action: "Build a small project", timeEstimate: "3 hours", priority: "medium" }
  ],
  totalTime: "5 hours",
  summary: "A practical approach to learning TypeScript through reading and building."
};

console.log(formatPlan(samplePlan));

What does this code do?

This file imports both your interface and your helper function, then puts them to work:

  • samplePlan is typed as ActionPlan. If you misspell a property or use the wrong type, TypeScript flags it immediately.
  • formatPlan(samplePlan) passes the typed plan into the formatter and returns the pretty-printed string.
  • Save src/index.ts.
  • Run your program by executing this command:
npx tsx src/index.ts

You should see a formatted action plan in your terminal with a goal, numbered steps, time estimates, priority icons, and a summary.

How do types protect you?

Try misspelling a property name in your samplePlan object. For example, change stepNumber to stepNum. Your editor will immediately show a red underline with an error like Property 'stepNumber' is missing in type. Undo the change when you're done experimenting.

Not seeing any output?

Double check that you removed the old console.log("Hello TypeScript!") line. The file should start with the import statements now.

If you see an error about modules not being found, confirm both src/types.ts and src/helpers.ts exist and are saved.

Why is my TypeScript program not finding my modules?

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files.

ⓧ I'd like to double check the full code

Here's what each file should look like at this point:

src/types.ts

export interface ActionStep {
  stepNumber: number;
  action: string;
  timeEstimate: string;
  priority: "high" | "medium" | "low";
}

export interface ActionPlan {
  goal: string;
  steps: ActionStep[];
  totalTime: string;
  summary: string;
}

src/helpers.ts

import type { ActionPlan } from "./types.js";

export function formatPlan(plan: ActionPlan): string {
  let output = `\n Goal: ${plan.goal}\n`;
  output += ` Total Time: ${plan.totalTime}\n`;
  output += `─────────────────────────────────\n`;

  for (const step of plan.steps) {
    const priorityLabel =
      step.priority === "high" ? "[!]" :
      step.priority === "medium" ? "[~]" : "[ ]";

    output += `  ${priorityLabel} Step ${step.stepNumber}: ${step.action}\n`;
    output += `      Time: ${step.timeEstimate}\n\n`;
  }

  output += `─────────────────────────────────\n`;
  output += ` Summary: ${plan.summary}\n`;

  return output;
}

src/index.ts

import type { ActionPlan } from "./types.js";
import { formatPlan } from "./helpers.js";

const samplePlan: ActionPlan = {
  goal: "Learn TypeScript",
  steps: [
    { stepNumber: 1, action: "Read the TypeScript handbook", timeEstimate: "2 hours", priority: "high" },
    { stepNumber: 2, action: "Build a small project", timeEstimate: "3 hours", priority: "medium" }
  ],
  totalTime: "5 hours",
  summary: "A practical approach to learning TypeScript through reading and building."
};

console.log(formatPlan(samplePlan));

Your types are locked in and your formatter is working. Next up, you'll connect to the Claude API and replace this hardcoded sample with a real AI-generated plan.

Connect to the Claude API

Your types and formatting function are working perfectly with hardcoded data. Now it's time to replace that sample plan with something real.

In this step, you'll connect to the Claude API so your tool can generate action plans on demand. You'll set up authentication, write the function that talks to Claude, and wire everything together into a working CLI.

In this step, get ready to:
  • Set up your API key as an environment variable.
  • Write the API call function that sends goals to Claude.
  • Wire everything together and run your first real AI-generated plan.
Set up your API key as an environment variable

The Anthropic SDK reads your API key from an environment variable called ANTHROPIC_API_KEY. You need to set this in your terminal before running your tool.

  • Set your API key in the terminal by running the command for your operating system:

macOS

export ANTHROPIC_API_KEY="your-api-key-here"
  • Replace your-api-key-here with the actual API key from your Anthropic account.

Windows

set ANTHROPIC_API_KEY=your-api-key-here
  • Replace your-api-key-here with the actual API key from your Anthropic account.
  • Do not add quotes around the value in Command Prompt.

Why an environment variable?

Storing your key in an environment variable keeps it out of your source code. If you accidentally push code to GitHub, your key stays safe because it only exists in your terminal session.

Now you need a tiny module that initializes the SDK client. The client object is what you'll use to make API calls throughout your project.

  • Create a new file called src/client.ts and add the following code:
import Anthropic from "@anthropic-ai/sdk";

export const client = new Anthropic();

What does this code do?

The new Anthropic() constructor automatically reads the ANTHROPIC_API_KEY environment variable you just set. You don't need to pass the key manually.

Exporting the client lets other files in your project import and reuse this single instance.

  • Save src/client.ts.
Write the API call function

Before writing the function that calls Claude, you need a way to verify that what Claude returns actually matches your ActionPlan shape. Claude returns JSON as a string, and TypeScript's compile-time types can't protect you from malformed runtime data.

  • Open src/helpers.ts and add the following validatePlan function below your existing formatPlan function:
export function validatePlan(data: unknown): data is ActionPlan {
  if (typeof data !== "object" || data === null) return false;

  const obj = data as Record<string, unknown>;

  if (typeof obj.goal !== "string") return false;
  if (typeof obj.totalTime !== "string") return false;
  if (typeof obj.summary !== "string") return false;
  if (!Array.isArray(obj.steps)) return false;

  for (const step of obj.steps) {
    if (typeof step !== "object" || step === null) return false;
    if (typeof step.stepNumber !== "number") return false;
    if (typeof step.action !== "string") return false;
    if (typeof step.timeEstimate !== "string") return false;
    if (!["high", "medium", "low"].includes(step.priority)) return false;
  }

  return true;
}

What is a type guard?

The data is ActionPlan return type is a TypeScript type guard. When this function returns true, TypeScript knows that data is safely an ActionPlan from that point forward.

This bridges the gap between compile-time types and runtime reality. The JSON coming back from Claude could be anything, so you check every field manually before trusting it.

  • Save src/helpers.ts.

Now you can write the main planning function that calls Claude and validates the response.

  • Create a new file called src/planner.ts and add the following code:
import { client } from "./client.js";
import { validatePlan } from "./helpers.js";
import type { ActionPlan } from "./types.js";

const SYSTEM_PROMPT = `You are a task planning assistant. When given a goal, respond with ONLY valid JSON matching this exact structure (no markdown, no code fences, just raw JSON):

{
  "goal": "the user's goal restated clearly",
  "steps": [
    {
      "stepNumber": 1,
      "action": "specific actionable step",
      "timeEstimate": "estimated time like '30 minutes' or '2 hours'",
      "priority": "high" | "medium" | "low"
    }
  ],
  "totalTime": "total estimated time",
  "summary": "brief summary of the overall approach"
}

Provide 4-6 practical, actionable steps. Be specific and realistic with time estimates.`;

export async function generatePlan(goal: string): Promise<ActionPlan> {
  const message = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    system: SYSTEM_PROMPT,
    messages: [
      {
        role: "user",
        content: goal,
      },
    ],
  });

  const textBlock = message.content[0];
  if (textBlock.type !== "text") {
    throw new Error("Unexpected response type from Claude");
  }

  const parsed: unknown = JSON.parse(textBlock.text);

  if (!validatePlan(parsed)) {
    throw new Error("Claude returned an invalid plan structure");
  }

  return parsed;
}

What does this code do?

The SYSTEM_PROMPT tells Claude exactly what JSON shape to return. This is how you get structured output from an AI model.

The generatePlan function calls client.messages.create() with the model claude-haiku-4-5 (fast and affordable for this task), parses the response as JSON, validates it matches your ActionPlan interface, and returns the typed result.

Notice the .js extensions in the import paths. TypeScript with ES modules requires these. TypeScript resolves .js imports to the corresponding .ts files at compile time.

  • Save src/planner.ts.

Seeing red squiggles in your editor?

If your editor shows errors on the import paths, make sure you're using .js extensions (not .ts). This is required when module is set to nodenext in your tsconfig.json.

Help me troubleshoot my TypeScript import errors

Wire it all together

Your planner module is ready. Now you need to update src/index.ts to accept a goal from the command line, send it to Claude, and display the result.

  • Replace the entire contents of src/index.ts with the following code:
import { generatePlan } from "./planner.js";
import { formatPlan } from "./helpers.js";

async function main(): Promise<void> {
  const goal = process.argv[2];

  if (!goal) {
    console.log("Usage: npx tsx src/index.ts \"Your goal here\"");
    console.log("Example: npx tsx src/index.ts \"Learn AWS in 2 weeks\"");
    process.exit(1);
  }

  process.stdout.write(`\nPlanning: "${goal}"...\n\n`);

  const plan = await generatePlan(goal);
  console.log(formatPlan(plan));
}

main();

What does this code do?

The process.argv[2] reads the first argument you pass after the script name. For example, in npx tsx src/index.ts "Learn AWS", the goal is Learn AWS.

If no goal is provided, the script prints usage instructions and exits. Otherwise, it sends the goal to generatePlan and prints the formatted result.

  • Save src/index.ts.
  • Run your AI Task Automator with a real goal by running this command:
npx tsx src/index.ts "Learn AWS in 2 weeks"

You should see a structured action plan with numbered steps, time estimates, and priorities. All generated by Claude in real time.

Seeing an authentication error?

Make sure you set the ANTHROPIC_API_KEY environment variable in the same terminal session where you're running the command. If you opened a new terminal window, you'll need to set it again.

Also confirm your API key is correct and your Anthropic account has available credits.

Help me fix this authentication error

✔️ Awesome, I've got everything!

Great. Double check you've saved all four files before moving on.

ⓧ I'd like to double check the full code

Here is src/client.ts:

import Anthropic from "@anthropic-ai/sdk";

export const client = new Anthropic();

Here is src/helpers.ts:

import type { ActionPlan } from "./types.js";

export function formatPlan(plan: ActionPlan): string {
  let output = `\n Goal: ${plan.goal}\n`;
  output += ` Total Time: ${plan.totalTime}\n`;
  output += `─────────────────────────────────\n`;

  for (const step of plan.steps) {
    const priorityLabel =
      step.priority === "high" ? "[!]" :
      step.priority === "medium" ? "[~]" : "[ ]";

    output += `  ${priorityLabel} Step ${step.stepNumber}: ${step.action}\n`;
    output += `      Time: ${step.timeEstimate}\n\n`;
  }

  output += `─────────────────────────────────\n`;
  output += ` Summary: ${plan.summary}\n`;

  return output;
}

export function validatePlan(data: unknown): data is ActionPlan {
  if (typeof data !== "object" || data === null) return false;

  const obj = data as Record<string, unknown>;

  if (typeof obj.goal !== "string") return false;
  if (typeof obj.totalTime !== "string") return false;
  if (typeof obj.summary !== "string") return false;
  if (!Array.isArray(obj.steps)) return false;

  for (const step of obj.steps) {
    if (typeof step !== "object" || step === null) return false;
    if (typeof step.stepNumber !== "number") return false;
    if (typeof step.action !== "string") return false;
    if (typeof step.timeEstimate !== "string") return false;
    if (!["high", "medium", "low"].includes(step.priority)) return false;
  }

  return true;
}

Here is src/planner.ts:

import { client } from "./client.js";
import { validatePlan } from "./helpers.js";
import type { ActionPlan } from "./types.js";

const SYSTEM_PROMPT = `You are a task planning assistant. When given a goal, respond with ONLY valid JSON matching this exact structure (no markdown, no code fences, just raw JSON):

{
  "goal": "the user's goal restated clearly",
  "steps": [
    {
      "stepNumber": 1,
      "action": "specific actionable step",
      "timeEstimate": "estimated time like '30 minutes' or '2 hours'",
      "priority": "high" | "medium" | "low"
    }
  ],
  "totalTime": "total estimated time",
  "summary": "brief summary of the overall approach"
}

Provide 4-6 practical, actionable steps. Be specific and realistic with time estimates.`;

export async function generatePlan(goal: string): Promise<ActionPlan> {
  const message = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    system: SYSTEM_PROMPT,
    messages: [
      {
        role: "user",
        content: goal,
      },
    ],
  });

  const textBlock = message.content[0];
  if (textBlock.type !== "text") {
    throw new Error("Unexpected response type from Claude");
  }

  const parsed: unknown = JSON.parse(textBlock.text);

  if (!validatePlan(parsed)) {
    throw new Error("Claude returned an invalid plan structure");
  }

  return parsed;
}

Here is src/index.ts:

import { generatePlan } from "./planner.js";
import { formatPlan } from "./helpers.js";

async function main(): Promise<void> {
  const goal = process.argv[2];

  if (!goal) {
    console.log("Usage: npx tsx src/index.ts \"Your goal here\"");
    console.log("Example: npx tsx src/index.ts \"Learn AWS in 2 weeks\"");
    process.exit(1);
  }

  process.stdout.write(`\nPlanning: "${goal}"...\n\n`);

  const plan = await generatePlan(goal);
  console.log(formatPlan(plan));
}

main();

Your AI Task Automator is alive. Next up, you'll add proper error handling so the tool gives helpful feedback when things go wrong.

Add Input Validation and Error Handling

Your tool generates action plans from Claude, but what happens when things go wrong? If the API key is missing, the network drops, or Claude returns malformed data, right now the program crashes with an unhelpful stack trace.

In this step, you'll add structured error handling that gives clear, actionable feedback for each failure case. This is what separates a rough prototype from a tool you can actually rely on.

In this step, get ready to:
  • Understand how runtime validation protects against untyped API responses.
  • Add environment checks and structured error handling to your main program.
  • Test multiple failure scenarios to confirm your error messages work.
Understand your runtime type guard

Your validatePlan function in src/helpers.ts is a type guard. It solves a critical problem in TypeScript: data coming from an external API cannot be trusted at compile time.

  • Open src/helpers.ts and scroll down to the validatePlan function.
export function validatePlan(data: unknown): data is ActionPlan {
  if (typeof data !== "object" || data === null) return false;

  const obj = data as Record<string, unknown>;

  if (typeof obj.goal !== "string") return false;
  if (typeof obj.totalTime !== "string") return false;
  if (typeof obj.summary !== "string") return false;
  if (!Array.isArray(obj.steps)) return false;

  for (const step of obj.steps) {
    if (typeof step !== "object" || step === null) return false;
    if (typeof step.stepNumber !== "number") return false;
    if (typeof step.action !== "string") return false;
    if (typeof step.timeEstimate !== "string") return false;
    if (!["high", "medium", "low"].includes(step.priority)) return false;
  }

  return true;
}

What is a type guard?

TypeScript can check types at compile time, but it has no way to verify what an API actually sends at runtime. The function signature data is ActionPlan tells TypeScript: if this function returns true, you can safely treat data as an ActionPlan from that point forward.

Without this guard, a malformed JSON response from Claude would silently pass into your formatting logic and crash somewhere confusing. The guard catches it early with a clear message.

Your src/planner.ts already calls validatePlan after parsing Claude's response. Now you'll add handling in src/index.ts so that when the guard throws, the user sees a helpful message instead of a raw stack trace.

Add environment and error checks to your main file

Right now, if someone runs your tool without setting their API key, the Anthropic SDK throws an opaque authentication error. You'll add a check that catches this early and tells the user exactly what to do.

  • In src/index.ts, find the closing brace of the if (!goal) block.
  • Add this new check directly below it:
  // Check that the API key is available before making any requests
  if (!process.env.ANTHROPIC_API_KEY) {
    console.error("Error: ANTHROPIC_API_KEY environment variable is not set.");
    console.error("Set it with: set ANTHROPIC_API_KEY=your-key-here");
    process.exit(1);
  }

What does this code do?

This checks process.env.ANTHROPIC_API_KEY before the program reaches the API call. If the variable is missing, the user sees clear setup instructions instead of a cryptic SDK error.

The Anthropic SDK reads this environment variable automatically when you create the client. Checking it yourself first lets you control the error message.

Next, you'll wrap the API call itself in error handling. The call to generatePlan can fail for several reasons: network issues, invalid JSON from Claude, or a rejected API key. Each needs a different message.

  • In src/index.ts, find these two lines near the bottom of the main function:
  const plan = await generatePlan(goal);
  console.log(formatPlan(plan));
  • Replace those two lines with this try/catch block:
  try {
    const plan = await generatePlan(goal);
    console.log(formatPlan(plan));
  } catch (error) {
    if (error instanceof SyntaxError) {
      // JSON.parse failed - Claude returned something that isn't valid JSON
      console.error("Error: Could not parse Claude's response as JSON.");
    } else if (error instanceof Error) {
      // SDK errors, network failures, or validation errors
      console.error(`Error: ${error.message}`);
    } else {
      // Catch-all for anything unexpected
      console.error("An unexpected error occurred.");
    }
    process.exit(1);
  }

What does this code do?

The try/catch wraps the API call so any thrown error is caught and handled gracefully.

  • A SyntaxError means JSON.parse failed. Claude occasionally returns markdown fences around JSON, which breaks parsing.
  • A generic Error covers SDK authentication failures, network timeouts, and your own validatePlan rejection.
  • The else branch handles the rare case where something other than an Error object is thrown.
  • Save src/index.ts.

Seeing red squiggles in your editor?

If your editor shows a type error on the error variable, make sure the catch (error) parameter is untyped. TypeScript strict mode infers it as unknown, which is why you check instanceof before accessing .message.

help me fix a TypeScript type error in my catch block

✔️ Awesome, I've got everything!

Great. Double check you've saved src/index.ts before moving on.

ⓧ I'd like to double check the full code

import { generatePlan } from "./planner.js";
import { formatPlan } from "./helpers.js";

async function main(): Promise<void> {
  const goal = process.argv[2];

  if (!goal) {
    console.log("Usage: npx tsx src/index.ts \"Your goal here\"");
    console.log("Example: npx tsx src/index.ts \"Learn AWS in 2 weeks\"");
    process.exit(1);
  }

  if (!process.env.ANTHROPIC_API_KEY) {
    console.error("Error: ANTHROPIC_API_KEY environment variable is not set.");
    console.error("Set it with: set ANTHROPIC_API_KEY=your-key-here");
    process.exit(1);
  }

  process.stdout.write(`\nPlanning: "${goal}"...\n\n`);

  try {
    const plan = await generatePlan(goal);
    console.log(formatPlan(plan));
  } catch (error) {
    if (error instanceof SyntaxError) {
      console.error("Error: Could not parse Claude's response as JSON.");
    } else if (error instanceof Error) {
      console.error(`Error: ${error.message}`);
    } else {
      console.error("An unexpected error occurred.");
    }
    process.exit(1);
  }
}

main();
Test the error cases

Good error handling is only useful if it actually works. You'll run through three scenarios to confirm your messages display correctly.

  • Test the missing-argument case by running this command with no goal:
npx tsx src/index.ts

You should see the usage instructions printed to your terminal, starting with Usage: npx tsx src/index.ts "Your goal here".

  • Now test the happy path by running with a real goal:
npx tsx src/index.ts "Learn AWS in 2 weeks"

You should see the structured action plan with numbered steps, time estimates, and priorities, just like before.

What about testing a wrong API key?

If you want to test the authentication error path, you can temporarily set a fake key in your terminal with set ANTHROPIC_API_KEY=fake-key and re-run the command. You'll see a clear error message from the SDK instead of a crash.

Remember to set your real key back afterwards so your tool keeps working.

Still seeing a raw stack trace?

Make sure the try/catch wraps both the generatePlan and console.log(formatPlan(plan)) lines. If either is outside the try block, errors from that line will bypass your handler.

help me debug why my error handling isn't catching errors

Your CLI tool now handles every realistic failure mode gracefully. Next up, you'll tackle the Secret Mission and add conversation memory so you can ask follow-up questions about your plan.

Secret mission

Add Conversation Memory

Your task automator generates a plan and exits. What if you could ask follow-up questions about any step? In this secret mission, you will add multi-turn conversation memory so Claude remembers the full context of your discussion.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally, so there are no ongoing costs. The only expense was Claude API usage (a few cents at most), which is charged per request and does not recur.

Resources you used:

  • The ai-task-automator project folder on your Desktop (contains all source files and node_modules).
  • The ANTHROPIC_API_KEY environment variable set in your terminal session.

Keep everything running

No action needed. Choose this if you are still actively building or want to run the tool again later.

  • Your project files stay on your Desktop in the ai-task-automator folder.
  • The ANTHROPIC_API_KEY environment variable only lasts for your current terminal session. If you close and reopen your terminal, you will need to set it again.
  • There is no ongoing cost. You are only charged when you run the tool and it makes API requests.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files stay on disk for when you return.

  • Close the terminal window you used for this project.
  • Your project folder and all files remain on your Desktop. No cost accrues while the tool is idle.
  • When you return, open a new terminal, navigate to the ai-task-automator folder, and set your ANTHROPIC_API_KEY again.

Delete - I don't want to use this again

Remove all project files from your machine. This frees up disk space and removes the installed packages.

  • Delete the project folder by running the appropriate command for your operating system.

On macOS or Linux:

cd ~/Desktop
rm -rf ai-task-automator

On Windows (Command Prompt):

cd %USERPROFILE%\Desktop
rmdir /s /q ai-task-automator
  • The ANTHROPIC_API_KEY environment variable is session-scoped. It disappears automatically when you close your terminal.
  • Your Anthropic account and API key remain active. If you want to revoke the key, visit the API Keys page in your Anthropic console and delete the key you created.

Nice Work!

Nice Work!

You just built a fully typed CLI tool that turns any goal into a structured action plan, powered by Claude. That is real TypeScript and real AI integration working together.

Here is what you accomplished:

  • Set up a TypeScript project from scratch with tsconfig.json, ES module support, and custom interfaces to model structured data.
  • Integrated the Anthropic Claude API using the official TypeScript SDK with a system prompt that returns typed JSON responses.
  • Built a working CLI tool with runtime type validation using type guards that generates AI-powered action plans from any goal.
  • Secret Mission: Added multi-turn conversation memory so you can ask follow-up questions about your plan.

Ready to quiz yourself?