Mastra AI Framework: Build Intelligent Agents & Workflows in TypeScript

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

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@latest

Follow 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 install

Create your .env file:

OPENAI_API_KEY=sk-your-key-here

Step 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

PropertyDescription
nameDisplay name for the agent
instructionsSystem prompt guiding the agent's behavior
modelLLM to use (supports OpenAI, Anthropic, Google, etc.)
toolsObject 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

  1. Write clear descriptions — the LLM reads these to decide when to use each tool
  2. Use Zod .describe() on every field — this tells the model what each parameter means
  3. Keep tools focused — one tool, one responsibility
  4. Handle errors gracefully — return error messages the agent can reason about
  5. 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.ts

You 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/openai

Create 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 dev

This 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 deploy

Mastra 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:

  1. Agents are the core unit — they reason about goals and use tools
  2. Tools extend agent capabilities with validated, typed functions
  3. Workflows provide deterministic multi-step pipelines
  4. RAG grounds agent responses in your own data
  5. Memory maintains context across conversations
  6. 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.


Want to read more tutorials? Check out our latest tutorial on Implementing RAG on PDFs Using File Search in the Responses API.

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