writing/tutorial/2026/05
TutorialMay 25, 2026·30 min read

Building Production-Ready AI Agents with Mastra and TypeScript

Learn how to build, deploy, and scale production-ready AI agents using Mastra, the TypeScript-first AI framework with 22K+ GitHub stars. Covers agents, tools, memory, workflows, and RAG.

Introduction

Mastra is the TypeScript-first framework for building AI agents, workflows, and RAG pipelines. Launched by the creators of Gatsby.js and backed by Y Combinator (W25, $13M), it reached 22,000+ GitHub stars and 300,000+ weekly npm downloads by its 1.0 release in January 2026. Unlike Python-first libraries such as LangChain, Mastra is built from the ground up for the TypeScript/JavaScript ecosystem.

In this tutorial you will build a customer support AI agent that can:

  • Search a knowledge base (RAG)
  • Call external APIs as tools
  • Maintain conversation memory across sessions
  • Route complex tasks through a multi-step workflow

Prerequisites

Before starting, ensure you have:

  • Node.js 20+ installed (node -v to verify)
  • A package manager: npm, pnpm, yarn, or bun
  • An LLM API key (OpenAI, Anthropic, or any of the 94+ supported providers)
  • Basic TypeScript knowledge

What You'll Build

A full-featured customer support agent named SupportBot that handles user questions, queries a product FAQ knowledge base via RAG, escalates complex tickets through a structured workflow, and remembers past conversations.

Step 1: Project Setup

Use the Mastra CLI to scaffold a new project:

npm create mastra@latest

The wizard will ask for:

  • Project namesupport-agent
  • Provider — choose OpenAI or Anthropic
  • Default modelgpt-4o or claude-sonnet-4-6
  • Components — select Agents, Tools, Workflows, and Memory

Navigate to your project and install dependencies:

cd support-agent
npm install

Your project structure will look like this:

support-agent/
├── src/
│   └── mastra/
│       ├── agents/
│       │   └── support-agent.ts
│       ├── tools/
│       │   └── ticket-tool.ts
│       ├── workflows/
│       │   └── escalation-workflow.ts
│       └── index.ts
├── .env
├── package.json
└── tsconfig.json

Add your API key to .env:

OPENAI_API_KEY=sk-...
# or
ANTHROPIC_API_KEY=sk-ant-...

Step 2: Create Your First Agent

Open src/mastra/agents/support-agent.ts and define the agent:

import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
import { ticketTool, knowledgeBaseTool } from "../tools/ticket-tool";
 
export const supportAgent = new Agent({
  name: "SupportBot",
  instructions: `You are a helpful customer support agent for the Noqta platform.
 
  Your responsibilities:
  - Answer questions about the platform using the knowledge base tool
  - Create support tickets for unresolved issues
  - Be empathetic, concise, and solution-focused
  - Escalate to a human if the issue is critical or technical
 
  Always greet the user by name if known.`,
  model: openai("gpt-4o"),
  tools: {
    ticketTool,
    knowledgeBaseTool,
  },
});

Step 3: Build Custom Tools with Zod Validation

Tools give your agent real-world capabilities. Each tool uses Zod for type-safe input/output validation.

Create src/mastra/tools/ticket-tool.ts:

import { createTool } from "@mastra/core/tools";
import { z } from "zod";
 
export const knowledgeBaseTool = createTool({
  id: "search-knowledge-base",
  description: "Search the product FAQ and documentation to answer user questions",
  inputSchema: z.object({
    query: z.string().describe("The user's question to search for"),
    limit: z.number().optional().default(3).describe("Number of results to return"),
  }),
  outputSchema: z.object({
    results: z.array(
      z.object({
        title: z.string(),
        content: z.string(),
        relevance: z.number(),
      })
    ),
  }),
  execute: async ({ context }) => {
    const { query, limit } = context;
    // In production, this calls your vector database
    const results = await searchVectorDB(query, limit);
    return { results };
  },
});
 
export const ticketTool = createTool({
  id: "create-ticket",
  description: "Create a support ticket when an issue cannot be resolved immediately",
  inputSchema: z.object({
    title: z.string().describe("Short summary of the issue"),
    description: z.string().describe("Detailed description of the problem"),
    priority: z.enum(["low", "medium", "high", "critical"]).describe("Issue priority level"),
    userEmail: z.string().email().describe("Customer email address"),
  }),
  outputSchema: z.object({
    ticketId: z.string(),
    status: z.string(),
    estimatedResolution: z.string(),
  }),
  execute: async ({ context }) => {
    const { title, description, priority, userEmail } = context;
    const ticket = await createSupportTicket({ title, description, priority, userEmail });
    return {
      ticketId: ticket.id,
      status: "created",
      estimatedResolution: priority === "critical" ? "2 hours" : "24 hours",
    };
  },
});
 
// Replace with real implementations
async function searchVectorDB(query: string, limit: number) {
  return [{ title: "FAQ", content: "Sample answer for: " + query, relevance: 0.9 }];
}
 
async function createSupportTicket(data: object) {
  return { id: "TKT-" + Math.random().toString(36).substr(2, 8).toUpperCase() };
}

Step 4: Add Persistent Memory

Mastra's memory system lets agents remember past conversations across sessions. Configure it with a storage backend:

import { Agent } from "@mastra/core/agent";
import { Memory } from "@mastra/memory";
import { LibSQLStore } from "@mastra/memory/storage/libsql";
import { openai } from "@ai-sdk/openai";
 
const memory = new Memory({
  storage: new LibSQLStore({
    url: "file:./support-memory.db",
    // For production: url: process.env.DATABASE_URL
  }),
  options: {
    lastMessages: 20,        // Include last 20 messages in context
    semanticRecall: {
      topK: 5,               // Retrieve 5 most relevant memories
      messageRange: 2,       // Context window around recalled memories
    },
  },
});
 
export const supportAgent = new Agent({
  name: "SupportBot",
  instructions: "...",
  model: openai("gpt-4o"),
  memory,
  tools: { ticketTool, knowledgeBaseTool },
});

Call the agent with a thread ID to maintain session continuity:

const response = await supportAgent.generate(
  "My payment failed but I was still charged",
  {
    resourceId: "user-123",   // Identifies the user
    threadId: "session-456",  // Identifies the conversation thread
  }
);
 
console.log(response.text);

Subsequent calls with the same threadId will have access to the full conversation history.

Step 5: Build a Multi-Step Escalation Workflow

For complex tasks that need a predictable execution path, use Mastra's workflow engine:

import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";
 
const analyzeTicketStep = createStep({
  id: "analyze-ticket",
  description: "Classify ticket priority and category",
  inputSchema: z.object({
    userMessage: z.string(),
    userEmail: z.string().email(),
  }),
  outputSchema: z.object({
    priority: z.enum(["low", "medium", "high", "critical"]),
    category: z.string(),
    needsEscalation: z.boolean(),
  }),
  execute: async ({ inputData }) => {
    const classification = await classifyWithLLM(inputData.userMessage);
    return classification;
  },
});
 
const routeTicketStep = createStep({
  id: "route-ticket",
  description: "Route ticket to the correct team",
  inputSchema: z.object({
    priority: z.enum(["low", "medium", "high", "critical"]),
    category: z.string(),
    needsEscalation: z.boolean(),
  }),
  outputSchema: z.object({
    assignedTeam: z.string(),
    ticketId: z.string(),
  }),
  execute: async ({ inputData }) => {
    const team = inputData.needsEscalation
      ? "tier-2-engineering"
      : "tier-1-support";
    const ticketId = await assignToTeam(team, inputData);
    return { assignedTeam: team, ticketId };
  },
});
 
const notifyUserStep = createStep({
  id: "notify-user",
  description: "Send confirmation email to user",
  inputSchema: z.object({
    assignedTeam: z.string(),
    ticketId: z.string(),
  }),
  outputSchema: z.object({
    notificationSent: z.boolean(),
  }),
  execute: async ({ inputData }) => {
    await sendConfirmationEmail(inputData.ticketId, inputData.assignedTeam);
    return { notificationSent: true };
  },
});
 
export const escalationWorkflow = createWorkflow({
  name: "ticket-escalation",
  triggerSchema: z.object({
    userMessage: z.string(),
    userEmail: z.string().email(),
  }),
})
  .then(analyzeTicketStep)
  .then(routeTicketStep)
  .then(notifyUserStep)
  .commit();
 
// Replace with real implementations
async function classifyWithLLM(message: string) {
  return { priority: "high" as const, category: "billing", needsEscalation: true };
}
async function assignToTeam(team: string, data: object) {
  return "TKT-" + Date.now();
}
async function sendConfirmationEmail(ticketId: string, team: string) {
  console.log(`Email sent for ${ticketId} to ${team}`);
}

Step 6: Register Everything with Mastra

Wire all components together in src/mastra/index.ts:

import { Mastra } from "@mastra/core";
import { supportAgent } from "./agents/support-agent";
import { escalationWorkflow } from "./workflows/escalation-workflow";
 
export const mastra = new Mastra({
  agents: { supportAgent },
  workflows: { escalationWorkflow },
  logger: {
    type: "CONSOLE",
    level: "INFO",
  },
});

Step 7: Test with Mastra Studio

Start the development server and open Mastra Studio:

npm run dev

Visit http://localhost:4111 in your browser. Mastra Studio provides:

  • Agent chat interface — test your agent with real messages
  • Workflow visualizer — see your workflow steps as a flowchart
  • Tool inspector — view tool calls and responses in real time
  • Memory browser — inspect stored conversation history

Test by sending: "My payment failed but I was still charged. My email is test@example.com."

Watch as it:

  1. Searches the knowledge base for payment issues
  2. Creates a support ticket automatically
  3. Returns an empathetic response with the ticket ID

Step 8: Deploy to Production

Mastra apps are standard Node.js servers. Deploy to any cloud:

Option A: Docker

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 4111
CMD ["npm", "start"]
npm install @mastra/vercel-functions
npx vercel deploy

Option C: Fly.io

fly launch --name support-agent
fly deploy

Set environment variables on your deployment platform:

# Fly.io
fly secrets set OPENAI_API_KEY=sk-...
 
# Vercel
vercel env add OPENAI_API_KEY

Testing Your Implementation

Write integration tests with @mastra/testing:

import { createTestRun } from "@mastra/testing";
import { mastra } from "./src/mastra";
 
const testRun = createTestRun({ mastra });
 
test("agent creates ticket for billing issues", async () => {
  const result = await testRun.agent("supportAgent").generate(
    "I was charged twice for the same subscription",
    { resourceId: "test-user", threadId: "test-thread" }
  );
 
  expect(result.toolCalls).toContainEqual(
    expect.objectContaining({ toolName: "create-ticket" })
  );
  expect(result.text).toContain("ticket");
});

Troubleshooting

Agent not calling tools: Ensure tool descriptions are detailed enough. The LLM decides whether to call a tool based on its description field — make it specific and action-oriented.

Memory not persisting: Check that resourceId and threadId are consistent between calls. Different IDs create separate memory threads.

Zod validation errors: When a tool's outputSchema doesn't match what execute returns, Mastra throws a validation error. Use z.infer to keep types aligned.

Studio not loading: Ensure port 4111 is free. Use MASTRA_PORT=4112 npm run dev to change the port.

Next Steps

Now that you have a working agent, consider:

Conclusion

Mastra brings the full TypeScript ecosystem to AI agent development — type safety, Zod validation, familiar tooling, and seamless Next.js/Node.js integration. By the end of this tutorial, you have built a production-ready support agent with tools, persistent memory, and a structured escalation workflow. This pattern scales from simple chatbots to fully autonomous multi-agent systems serving thousands of users.