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 -vto 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@latestThe wizard will ask for:
- Project name —
support-agent - Provider — choose OpenAI or Anthropic
- Default model —
gpt-4oorclaude-sonnet-4-6 - Components — select Agents, Tools, Workflows, and Memory
Navigate to your project and install dependencies:
cd support-agent
npm installYour 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 devVisit 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:
- Searches the knowledge base for payment issues
- Creates a support ticket automatically
- 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"]Option B: Vercel (recommended for Next.js integration)
npm install @mastra/vercel-functions
npx vercel deployOption C: Fly.io
fly launch --name support-agent
fly deploySet environment variables on your deployment platform:
# Fly.io
fly secrets set OPENAI_API_KEY=sk-...
# Vercel
vercel env add OPENAI_API_KEYTesting 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:
- Vector search with Pinecone — connect a real vector database for RAG
- Background jobs with Inngest — run long-running agent tasks asynchronously
- Stateful agents with LangGraph — compare workflow paradigms
- Explore the Mastra documentation for evals, observability, and multi-agent orchestration
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.