CrewAI Tutorial (TypeScript): handling async tools for advanced developers

By Cyprian AaronsUpdated 2026-04-21
crewaihandling-async-tools-for-advanced-developerstypescript

This tutorial shows you how to wire async tools into a CrewAI TypeScript agent without blocking the run loop or returning half-finished data. You need this when your tool calls external APIs, databases, or internal services that are naturally asynchronous and you want the agent to wait for real results instead of fake placeholders.

What You'll Need

  • Node.js 18+ installed
  • A TypeScript project with ts-node or a build step
  • crewai installed in your project
  • An LLM API key configured for your provider
    • Example: OPENAI_API_KEY
  • A valid model name supported by your CrewAI setup
  • Basic familiarity with:
    • Agent
    • Task
    • Crew
    • custom tools

Step-by-Step

  1. Start with a clean TypeScript project and install the dependencies you actually need. For this example, I’m using OpenAI as the model provider because it’s the most common setup in CrewAI TypeScript projects.
npm init -y
npm install crewai dotenv zod
npm install -D typescript ts-node @types/node
  1. Create a tool that performs async work and returns a string. The important part is that the tool method is async, and it always resolves to plain text that the agent can consume.
// src/tools/fetchCustomerStatus.ts
import { Tool } from "crewai";

export class FetchCustomerStatusTool extends Tool {
  name = "fetch_customer_status";
  description = "Fetches a customer's account status from an internal service.";

  async execute(customerId: string): Promise<string> {
    await new Promise((resolve) => setTimeout(resolve, 800));

    const statusMap: Record<string, string> = {
      CUST-1001: "active",
      CUST-1002: "delinquent",
      CUST-1003: "pending_verification",
    };

    return statusMap[customerId] ?? "unknown";
  }
}
  1. Define your agent and give it the async tool. Keep the instruction tight so the model knows when to call the tool and how to use its output.
// src/agent.ts
import { Agent } from "crewai";
import { FetchCustomerStatusTool } from "./tools/fetchCustomerStatus";

export function createSupportAgent() {
  return new Agent({
    name: "Support Analyst",
    role: "Customer support analyst",
    goal: "Check customer status before answering policy questions",
    backstory: "You work in insurance operations and must verify customer state before responding.",
    tools: [new FetchCustomerStatusTool()],
    verbose: true,
    llm: {
      provider: "openai",
      model: "gpt-4o-mini",
    },
  });
}
  1. Build a task that forces the agent to use the tool result in a concrete answer. This keeps the output deterministic enough for testing and avoids vague summaries.
// src/task.ts
import { Task } from "crewai";

export function createSupportTask() {
  return new Task({
    description:
      "Check customer CUST-1002 status using the available tool, then explain whether they can be offered expedited claim processing.",
    expectedOutput:
      "A short decision with the customer status and a recommendation based on that status.",
  });
}
  1. Run the crew from an entrypoint script. Load environment variables first, then execute the crew and print the final result.
// src/index.ts
import "dotenv/config";
import { Crew } from "crewai";
import { createSupportAgent } from "./agent";
import { createSupportTask } from "./task";

async function main() {
  const crew = new Crew({
    agents: [createSupportAgent()],
    tasks: [createSupportTask()],
    verbose: true,
  });

  const result = await crew.kickoff();
  console.log("\n=== FINAL RESULT ===\n");
  console.log(result);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
  1. If your real tool calls HTTP services, keep the same async shape but swap in fetch. The agent does not care whether you used a database driver, REST client, or SDK, as long as your tool resolves to a usable string.
// src/tools/fetchPolicyData.ts
import { Tool } from "crewai";

export class FetchPolicyDataTool extends Tool {
  name = "fetch_policy_data";
  description = "Fetches policy details from an external API.";

  async execute(policyId: string): Promise<string> {
    const response = await fetch(`https://api.example.com/policies/${policyId}`, {
      headers: { Authorization: `Bearer ${process.env.POLICY_API_KEY}` },
    });

    if (!response.ok) {
      return `error:${response.status}`;
    }

    const data = (await response.json()) as { status: string; premiumDue: boolean };
    return JSON.stringify(data);
  }
}

Testing It

Run the entrypoint with your environment variable set and watch for two things: the tool invocation in verbose logs and a final answer that references the returned status instead of guessing. If you see delinquent or another exact value coming back in the final response, your async path is working correctly.

For quick validation, change the customer ID in the task to one of the known IDs in the map and rerun it. Then change it to an unknown ID and confirm you get "unknown" back through the agent flow.

If you are integrating a real API, test failure paths too. A timeout or non-200 response should still return a controlled string so the agent can reason about it instead of crashing mid-run.

Next Steps

  • Add schema validation to tool inputs with zod
  • Wrap external calls with retries and timeouts
  • Chain multiple async tools in one task and compare their latency profiles

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