Mastra AI Framework: Build Intelligent Agents & Workflows in TypeScript

Build AI agents that actually do things. Mastra is the TypeScript-first framework from the team behind Gatsby — it gives you agents, workflows, tools, RAG, and evals in one stack with zero glue code. In this tutorial, you'll build a research assistant agent that searches the web, summarizes findings, and writes structured reports.
What You'll Learn
By the end of this tutorial, you'll be able to:
- Set up a Mastra project from scratch with TypeScript
- Create AI agents with instructions, model configuration, and tool access
- Build custom tools with input validation using Zod schemas
- Design multi-step workflows as directed graphs (DAGs)
- Implement a basic RAG pipeline for document-aware agents
- Test and evaluate agent outputs
- Deploy your Mastra application to production
Prerequisites
Before starting, make sure you have:
- Node.js 20+ installed (
node --version) - TypeScript experience (types, generics, async/await)
- An OpenAI API key (or Anthropic, Google, etc.)
- Basic understanding of LLMs and prompt engineering
- A code editor — VS Code or Cursor recommended
What is Mastra?
Mastra is an open-source TypeScript framework for building AI-powered applications. Created by the team behind Gatsby (backed by Y Combinator), it provides a batteries-included approach to building agentic apps with:
- Agents — autonomous LLM-powered entities that reason and use tools
- Tools — typed TypeScript functions the agent can call
- Workflows — directed graphs for multi-step processes
- RAG — retrieval-augmented generation with vector stores
- Evals — evaluate agent output quality
- Memory — conversation history and semantic memory
Unlike other frameworks that require YAML configuration or Python glue code, Mastra is pure TypeScript — you define everything in code with full type safety.
Step 1: Create a New Mastra Project
The fastest way to get started is with the official CLI:
npm create mastra@latestFollow the prompts:
- Project name:
research-assistant - Components: Select Agents, Tools, and Workflows
- Default model provider: OpenAI (or your preferred provider)
- Include example code: Yes
This scaffolds a complete project structure:
research-assistant/
├── src/
│ └── mastra/
│ ├── agents/
│ │ └── index.ts
│ ├── tools/
│ │ └── index.ts
│ ├── workflows/
│ │ └── index.ts
│ └── index.ts
├── .env
├── package.json
└── tsconfig.json
Now install dependencies and set up your environment:
cd research-assistant
npm installCreate your .env file:
OPENAI_API_KEY=sk-your-key-hereStep 2: Understand the Mastra Entry Point
The main configuration lives in src/mastra/index.ts. This is where you register agents, tools, and workflows:
import { Mastra } from "@mastra/core";
import { researchAgent } from "./agents";
import { searchTool, summarizeTool } from "./tools";
import { researchWorkflow } from "./workflows";
export const mastra = new Mastra({
agents: { researchAgent },
workflows: { researchWorkflow },
});The Mastra instance is the central orchestrator. It wires up all your components and exposes a type-safe REST API automatically.
Step 3: Build Your First Agent
An agent is an LLM-powered entity with instructions, model configuration, and access to tools. Let's create a research assistant agent.
Edit src/mastra/agents/index.ts:
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
import { searchTool, summarizeTool, reportTool } from "../tools";
export const researchAgent = new Agent({
name: "Research Assistant",
instructions: `You are a thorough research assistant. When given a topic:
1. Search for relevant, up-to-date information
2. Summarize key findings into concise points
3. Generate a structured report with sections and citations
Always verify information from multiple sources when possible.
Be factual and cite your sources. If unsure, say so.`,
model: openai("gpt-4o"),
tools: {
searchTool,
summarizeTool,
reportTool,
},
});Key Agent Properties
| Property | Description |
|---|---|
name | Display name for the agent |
instructions | System prompt guiding the agent's behavior |
model | LLM to use (supports OpenAI, Anthropic, Google, etc.) |
tools | Object of tools the agent can invoke |
The agent will automatically reason about which tools to use based on the user's input and the tool descriptions.
Step 4: Create Custom Tools
Tools are TypeScript functions that extend what your agent can do. Each tool has a description (so the LLM knows when to use it), an input schema (validated with Zod), and an execute function.
Edit src/mastra/tools/index.ts:
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
// Tool 1: Web Search
export const searchTool = createTool({
id: "web-search",
description: "Search the web for information on a given query. Returns relevant results with titles, snippets, and URLs.",
inputSchema: z.object({
query: z.string().describe("The search query"),
maxResults: z.number().default(5).describe("Maximum number of results to return"),
}),
execute: async ({ context }) => {
const { query, maxResults } = context;
// In production, use a real search API (Serper, Tavily, etc.)
const response = await fetch(
`https://api.tavily.com/search`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: process.env.TAVILY_API_KEY,
query,
max_results: maxResults,
}),
}
);
const data = await response.json();
return {
results: data.results.map((r: any) => ({
title: r.title,
url: r.url,
snippet: r.content,
})),
};
},
});
// Tool 2: Summarize Text
export const summarizeTool = createTool({
id: "summarize-text",
description: "Summarize a long piece of text into key bullet points.",
inputSchema: z.object({
text: z.string().describe("The text to summarize"),
maxPoints: z.number().default(5).describe("Maximum number of bullet points"),
}),
execute: async ({ context }) => {
const { text, maxPoints } = context;
// Simple extractive summary — in production, use an LLM call
const sentences = text.split(". ").filter((s) => s.length > 20);
const summary = sentences.slice(0, maxPoints).map((s) => s.trim());
return { summary };
},
});
// Tool 3: Generate Report
export const reportTool = createTool({
id: "generate-report",
description: "Generate a structured markdown report from research findings.",
inputSchema: z.object({
title: z.string().describe("Report title"),
sections: z.array(
z.object({
heading: z.string(),
content: z.string(),
sources: z.array(z.string()).optional(),
})
).describe("Report sections with headings, content, and optional source URLs"),
}),
execute: async ({ context }) => {
const { title, sections } = context;
let markdown = `# ${title}\n\n`;
markdown += `*Generated on ${new Date().toISOString().split("T")[0]}*\n\n`;
for (const section of sections) {
markdown += `## ${section.heading}\n\n`;
markdown += `${section.content}\n\n`;
if (section.sources?.length) {
markdown += `**Sources:**\n`;
for (const source of section.sources) {
markdown += `- ${source}\n`;
}
markdown += "\n";
}
}
return { report: markdown };
},
});Tool Design Best Practices
- Write clear descriptions — the LLM reads these to decide when to use each tool
- Use Zod
.describe()on every field — this tells the model what each parameter means - Keep tools focused — one tool, one responsibility
- Handle errors gracefully — return error messages the agent can reason about
- Add sensible defaults — use
.default()for optional parameters
Step 5: Design a Multi-Step Workflow
Workflows let you define deterministic pipelines as directed graphs. Unlike agent-driven decisions (which are non-deterministic), workflows guarantee execution order.
Create src/mastra/workflows/index.ts:
import { Workflow, Step } from "@mastra/core/workflows";
import { z } from "zod";
import { researchAgent } from "../agents";
// Step 1: Research a topic
const researchStep = new Step({
id: "research",
inputSchema: z.object({
topic: z.string(),
depth: z.enum(["shallow", "deep"]).default("deep"),
}),
execute: async ({ context }) => {
const agent = researchAgent;
const response = await agent.generate(
`Research the following topic thoroughly: ${context.topic}.
Depth level: ${context.depth}.
Use the search tool to find current information.`
);
return { findings: response.text };
},
});
// Step 2: Extract key insights
const analyzeStep = new Step({
id: "analyze",
inputSchema: z.object({
findings: z.string(),
}),
execute: async ({ context }) => {
const agent = researchAgent;
const response = await agent.generate(
`Analyze these research findings and extract 5-7 key insights:
${context.findings}
Format as a JSON array of objects with "insight" and "confidence" fields.`
);
return { insights: response.text };
},
});
// Step 3: Generate final report
const reportStep = new Step({
id: "report",
inputSchema: z.object({
topic: z.string(),
insights: z.string(),
}),
execute: async ({ context }) => {
const agent = researchAgent;
const response = await agent.generate(
`Create a comprehensive research report on "${context.topic}"
using these insights: ${context.insights}.
Use the report tool to generate a structured markdown document.`
);
return { report: response.text };
},
});
// Build the workflow graph
export const researchWorkflow = new Workflow({
name: "research-pipeline",
triggerSchema: z.object({
topic: z.string().describe("The topic to research"),
depth: z.enum(["shallow", "deep"]).default("deep"),
}),
});
researchWorkflow
.step(researchStep)
.then(analyzeStep)
.then(reportStep)
.commit();Workflow Features
- Type-safe data flow — output from one step feeds into the next with full TypeScript inference
- Parallel execution — use
.parallel()to run independent steps concurrently - Conditional branching — add
.if()/.else()for dynamic paths - Error handling — each step can define retry logic and fallbacks
- Suspension — workflows can pause and wait for human input
Step 6: Run Your Agent
Now let's test the agent interactively. Create a test script src/test.ts:
import { mastra } from "./mastra";
async function main() {
// Get the agent
const agent = mastra.getAgent("researchAgent");
// Simple text generation
const response = await agent.generate(
"What are the latest developments in quantum computing in 2026?"
);
console.log(response.text);
// Streaming response
const stream = await agent.stream(
"Compare the top 3 TypeScript AI frameworks in 2026"
);
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}
}
main().catch(console.error);Run it:
npx tsx src/test.tsYou should see the agent reasoning through your request, calling tools as needed, and producing a structured response.
Step 7: Execute the Workflow
Test the complete research pipeline:
import { mastra } from "./mastra";
async function runWorkflow() {
const workflow = mastra.getWorkflow("researchWorkflow");
const result = await workflow.execute({
triggerData: {
topic: "The state of WebAssembly in 2026",
depth: "deep",
},
});
// Access results from each step
console.log("Research:", result.results.research);
console.log("Analysis:", result.results.analyze);
console.log("Report:", result.results.report);
}
runWorkflow().catch(console.error);Step 8: Add RAG for Document-Aware Agents
RAG (Retrieval-Augmented Generation) lets your agent answer questions based on your own documents. Mastra integrates with vector stores like pgvector, Pinecone, and Qdrant.
Install the RAG dependencies:
npm install @mastra/rag @ai-sdk/openaiCreate a RAG pipeline:
import { embed, embedMany } from "ai";
import { openai } from "@ai-sdk/openai";
import { PgVector } from "@mastra/rag";
// 1. Initialize vector store
const vectorStore = new PgVector({
connectionString: process.env.DATABASE_URL!,
});
// 2. Create embeddings from your documents
const documents = [
"Mastra is a TypeScript framework for building AI agents...",
"Workflows in Mastra are directed acyclic graphs...",
"Tools extend agent capabilities with custom functions...",
];
const { embeddings } = await embedMany({
model: openai.embedding("text-embedding-3-small"),
values: documents,
});
// 3. Store embeddings in the vector database
await vectorStore.upsert({
indexName: "docs",
vectors: embeddings.map((embedding, i) => ({
id: `doc-${i}`,
values: embedding,
metadata: { text: documents[i] },
})),
});
// 4. Query similar documents
const { embedding: queryEmbedding } = await embed({
model: openai.embedding("text-embedding-3-small"),
value: "How do Mastra workflows work?",
});
const results = await vectorStore.query({
indexName: "docs",
queryVector: queryEmbedding,
topK: 3,
});Then create a RAG-aware tool for your agent:
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { embed } from "ai";
import { openai } from "@ai-sdk/openai";
export const ragSearchTool = createTool({
id: "search-knowledge-base",
description: "Search the internal knowledge base for relevant information.",
inputSchema: z.object({
query: z.string().describe("What to search for in the knowledge base"),
}),
execute: async ({ context }) => {
const { embedding } = await embed({
model: openai.embedding("text-embedding-3-small"),
value: context.query,
});
const results = await vectorStore.query({
indexName: "docs",
queryVector: embedding,
topK: 5,
});
return {
documents: results.map((r) => ({
text: r.metadata?.text,
score: r.score,
})),
};
},
});Step 9: Add Agent Memory
Memory gives your agent context across conversations. Mastra supports both conversation history and semantic memory:
import { Agent } from "@mastra/core/agent";
import { Memory } from "@mastra/memory";
import { openai } from "@ai-sdk/openai";
const memory = new Memory({
// Stores recent conversation messages
options: {
lastMessages: 20,
semanticRecall: {
topK: 3,
messageRange: 50,
},
},
});
export const assistantAgent = new Agent({
name: "Assistant",
instructions: "You are a helpful assistant with memory of past conversations.",
model: openai("gpt-4o"),
memory,
});When using memory, pass a threadId to maintain conversation context:
// First message
const res1 = await assistantAgent.generate("My name is Sarah", {
threadId: "user-123",
});
// Later message — the agent remembers
const res2 = await assistantAgent.generate("What's my name?", {
threadId: "user-123",
});
// Output: "Your name is Sarah"Step 10: Evaluate Agent Quality
Mastra includes built-in evaluation tools to measure agent output quality:
import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
const agent = new Agent({
name: "Evaluated Agent",
instructions: "Answer questions accurately and concisely.",
model: openai("gpt-4o"),
evals: {
completeness: {
type: "model-graded",
prompt: "Rate if the response fully addresses the question (0-1):",
},
conciseness: {
type: "model-graded",
prompt: "Rate the conciseness of the response (0-1):",
},
hallucination: {
type: "model-graded",
prompt: "Rate if the response contains made-up information (0=hallucinated, 1=factual):",
},
},
});Step 11: Deploy with the Mastra Server
Mastra automatically generates a REST API for your agents and workflows. Start the development server:
npx mastra devThis exposes endpoints like:
POST /api/agents/researchAgent/generate
POST /api/agents/researchAgent/stream
POST /api/workflows/researchWorkflow/execute
For production, build and deploy:
npx mastra build
npx mastra deployMastra supports deployment to:
- Node.js servers (Express, Fastify)
- Cloudflare Workers
- Vercel / Netlify serverless
- Docker containers
Using Multiple Model Providers
Mastra works with any AI SDK-compatible model provider. You can mix and match:
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { google } from "@ai-sdk/google";
// Use different models for different agents
const fastAgent = new Agent({
name: "Fast Responder",
instructions: "Quick, concise answers.",
model: openai("gpt-4o-mini"),
tools: { searchTool },
});
const deepAgent = new Agent({
name: "Deep Analyst",
instructions: "Thorough, detailed analysis.",
model: anthropic("claude-sonnet-4-20250514"),
tools: { searchTool, summarizeTool },
});
const visionAgent = new Agent({
name: "Image Analyzer",
instructions: "Analyze and describe images.",
model: google("gemini-2.0-flash"),
tools: {},
});Project Structure Best Practices
For production applications, organize your code like this:
src/mastra/
├── agents/
│ ├── research-agent.ts
│ ├── writing-agent.ts
│ └── index.ts # Re-exports all agents
├── tools/
│ ├── search.ts
│ ├── summarize.ts
│ ├── report.ts
│ └── index.ts # Re-exports all tools
├── workflows/
│ ├── research-pipeline.ts
│ ├── content-pipeline.ts
│ └── index.ts # Re-exports all workflows
└── index.ts # Mastra instance
Troubleshooting
Common Issues
"Tool not found" errors:
Make sure tools are passed to the agent's tools object, not just imported. The agent only has access to tools explicitly listed in its configuration.
Workflow steps not receiving data:
Check that the output schema of one step matches the input schema of the next. Use Zod's .parse() to debug type mismatches.
Memory not persisting:
Ensure you're passing the same threadId across related messages. Each thread maintains its own conversation history.
Rate limits with model providers: Add retry logic to your tools and consider using different models for different tasks (e.g., GPT-4o-mini for simple tool calls, Claude for deep analysis).
Next Steps
Now that you have a working Mastra application, consider exploring:
- Mastra Documentation — full API reference and guides
- Multi-agent systems — create agents that delegate tasks to other agents
- Custom integrations — connect to databases, APIs, and third-party services
- Production observability — add tracing with OpenTelemetry for debugging
- Voice agents — add speech-to-text and text-to-speech capabilities
Conclusion
Mastra brings structure and type safety to AI application development. Instead of wiring together disparate tools with glue code, you get a unified framework where agents, tools, workflows, RAG, and evals all work together seamlessly in TypeScript.
The key takeaways from this tutorial:
- Agents are the core unit — they reason about goals and use tools
- Tools extend agent capabilities with validated, typed functions
- Workflows provide deterministic multi-step pipelines
- RAG grounds agent responses in your own data
- Memory maintains context across conversations
- Evals ensure output quality stays high
With Mastra, you can go from prototype to production-ready AI application without leaving the TypeScript ecosystem. Start simple with a single agent and tool, then scale up to multi-agent workflows as your requirements grow.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Building an Autonomous AI Agent with Agentic RAG and Next.js
Learn how to build an AI agent that autonomously decides when and how to retrieve information from vector databases. A comprehensive hands-on guide using Vercel AI SDK and Next.js with executable examples.

Building AI Agents from Scratch with TypeScript: Master the ReAct Pattern Using the Vercel AI SDK
Learn how to build AI agents from the ground up using TypeScript. This tutorial covers the ReAct pattern, tool calling, multi-step reasoning, and production-ready agent loops with the Vercel AI SDK.

How to Create an MCP Server in TypeScript (2026) — Step-by-Step Tutorial
Step-by-step guide to building an MCP server with TypeScript and Node.js. Create tools, resources, and prompts using @modelcontextprotocol/sdk, then connect to Claude Desktop and Cursor.