CrewAI Tutorial (TypeScript): handling async tools for beginners

By Cyprian AaronsUpdated 2026-04-21
crewaihandling-async-tools-for-beginnerstypescript

This tutorial shows you how to build a CrewAI workflow in TypeScript where tools return promises and the agent waits for them correctly. You need this when your tool calls hit databases, HTTP APIs, or file systems and you don’t want race conditions or half-finished results.

What You'll Need

  • Node.js 18+ installed
  • A TypeScript project with ts-node or a build step
  • CrewAI TypeScript package installed
  • An OpenAI API key set in your environment
  • Basic familiarity with agents, tasks, and tools in CrewAI
  • A terminal that can run npm or pnpm

Step-by-Step

  1. Set up a minimal TypeScript project and install the packages you need. Keep the setup boring and explicit so the async behavior is easy to debug later.
mkdir crewai-async-tools-demo
cd crewai-async-tools-demo
npm init -y
npm install crewai zod dotenv openai
npm install -D typescript ts-node @types/node
npx tsc --init
  1. Add your environment variable and make TypeScript load it at runtime. The important part here is that your tool code will be async, so your app must be able to wait for network calls without exiting early.
# .env
OPENAI_API_KEY=your_openai_api_key_here
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist"
  }
}
  1. Create an async tool that simulates a real external call. In production, this would be an HTTP request or database query; here we use setTimeout so you can see the promise flow clearly.
// src/tools.ts
import { z } from "zod";
import { Tool } from "crewai";

export const fetchPolicyStatusTool = new Tool({
  name: "fetch_policy_status",
  description: "Fetches policy status by policy number.",
  schema: z.object({
    policyNumber: z.string().min(5),
  }),
  func: async ({ policyNumber }) => {
    await new Promise((resolve) => setTimeout(resolve, 800));
    return JSON.stringify({
      policyNumber,
      status: "Active",
      updatedAt: new Date().toISOString(),
    });
  },
});
  1. Wire the tool into an agent and make sure the task expects structured output from the tool call. The key beginner mistake is writing an async tool but forgetting that the agent prompt must clearly tell it when to use the tool.
// src/crew.ts
import { Agent, Task, Crew } from "crewai";
import { fetchPolicyStatusTool } from "./tools.js";

export const supportAgent = new Agent({
  name: "Support Agent",
  role: "Insurance support specialist",
  goal: "Answer policy status questions using tools when needed",
  backstory: "You help customers check their insurance policy status.",
  tools: [fetchPolicyStatusTool],
});

export const statusTask = new Task({
  description:
    "Check the status of policy number POL12345 and return a short customer-facing answer.",
  expectedOutput: "A concise answer with the policy status and timestamp.",
  agent: supportAgent,
});

export const crew = new Crew({
  agents: [supportAgent],
  tasks: [statusTask],
});
  1. Run the crew from an executable entrypoint and await the result. If you skip await, your process may exit before the tool finishes, which is exactly what beginners usually hit first.
// src/index.ts
import dotenv from "dotenv";
dotenv.config();

import { crew } from "./crew.js";

async function main() {
  const result = await crew.kickoff();
  console.log("\n=== Crew Result ===\n");
  console.log(result);
}

main().catch((error) => {
  console.error("Crew failed:", error);
  process.exit(1);
});
  1. Add a script and run it. At this point you should see the agent call the async tool, wait for the promise to resolve, and then print a response based on the returned JSON.
{
  "scripts": {
    "dev": "ts-node src/index.ts"
  }
}
npm run dev

Testing It

Run the app a few times and confirm that it consistently prints a response after roughly one second, not immediately. If you want to verify that the async path is real, increase the setTimeout delay in the tool to 3000 and watch whether the process still waits correctly.

A good sanity check is to change policyNumber to something invalid like "P1" and confirm Zod rejects it before any fake API work starts. That tells you your schema validation is happening before execution, which is what you want in production.

If you replace the timeout with a real fetch() call later, keep the same pattern: validate input with Zod, make the function async, return a stringified payload, and always await crew.kickoff().

Next Steps

  • Replace the mock timeout with a real REST API call using fetch
  • Add retry logic and timeouts inside your tool for flaky upstream services
  • Learn how to chain multiple tools in one task without blocking other work

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides