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"For deeper persistence across sessions and tools, open-source projects like GBrain — YC president Garry Tan's markdown + pgvector memory system — pair well with Mastra's @mastra/memory when you want a portable, self-hosted knowledge store outside the framework.
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.