Mastra: TypeScript Agents for Web Applications
Source: mortalapps.com- Mastra is a TypeScript-native framework designed to build, run, and deploy AI agents directly within modern web application stacks.
- It eliminates the architectural complexity and latency overhead of maintaining split-stack (Python/TypeScript) agent architectures.
- Provides compile-time type safety for tools, structured state machines for workflows, and native integration with Vercel and Next.js.
- Enables deployment of production-grade agents as serverless functions with minimal cold-start times and built-in OpenTelemetry tracing.
Why This Matters
Building a mastra ai agent typescript application solves a critical architectural friction point in modern web development. Historically, AI agent development has been dominated by Python-based ecosystems. When web engineering teams building on Next.js, Nuxt, or SvelteKit need to integrate agentic workflows, they are forced to maintain a split-stack architecture: a Python backend for agent execution and a TypeScript frontend. This split introduces significant operational overhead, including double serialization costs, complex cross-language type synchronization, and increased infrastructure footprint.
Mastra was invented to bring production-grade agent orchestration directly into the TypeScript ecosystem. By providing a native TypeScript runtime, Mastra allows developers to define agents, tools, and workflows in a single codebase that runs seamlessly in serverless environments like Vercel, AWS Lambda, or Cloudflare Workers. This approach eliminates the need for separate Python microservices, dramatically reducing cold-start times and simplifying deployment pipelines.
If you ignore TypeScript-native agent frameworks and stick to multi-language architectures for web-native apps, you risk severe performance degradation due to network hops between the web server and the Python agent service. Additionally, type safety is broken at the boundary, leading to runtime failures when tool schemas drift. Mastra should be used when your primary application stack is JavaScript/TypeScript and you require low-latency, serverless-compatible agent execution. For heavy data-science or machine-learning workloads that rely on Python-exclusive libraries, a split-stack architecture remains appropriate; however, for interactive web applications, Mastra offers a superior developer experience and operational efficiency.
Core Concepts
To build effective agents with Mastra, you must understand its core architectural primitives:
- Mastra Agent: The primary execution unit. It encapsulates system instructions, a configured LLM provider (via standard adapters), and a registered set of tools that the agent can autonomously decide to invoke.
- Tools: Strongly-typed executable functions. Each tool defines a strict input schema using Zod, which is automatically converted into JSON Schema for the LLM's function-calling interface, and an execution handler.
- Workflows: Directed Acyclic Graphs (DAGs) that orchestrate complex multi-step processes. Workflows allow you to chain agent actions, manage state transitions, and implement conditional branching with deterministic control.
- Mastra Engine: The runtime subsystem responsible for state persistence, vector database integrations, and execution logging. It manages the lifecycle of agent runs and workflow instances.
- Type-Safe Context: The compile-time verification of data flowing between tools, agents, and workflows, ensuring that runtime payload mismatches are caught during development.
How It Works
The execution lifecycle of a Mastra agent consists of five distinct phases, moving from client invocation to final response generation.
Phase 1: Initialization and Configuration
When the application starts or a serverless function is invoked, the Mastra runtime instantiates the configured Agent. This process loads the system instructions, initializes the LLM client adapter (e.g., OpenAI, Anthropic), and registers the tools. The tools' Zod schemas are compiled into JSON Schema format, ready to be sent to the LLM.
Phase 2: Input Ingestion and Context Assembly
The agent receives an input prompt or a trigger from a workflow. Mastra assembles the context window, combining the system prompt, current user input, and any historical conversation threads retrieved from the configured state store (such as PostgreSQL or Redis).
Phase 3: The Agentic Execution Loop
The assembled context is dispatched to the LLM. The LLM determines whether the user request requires external data or action. If the LLM requests a tool call, the Mastra runtime intercepts this request, validates the arguments against the tool's Zod schema, and executes the tool's handler locally within the application process. The tool's output is appended to the context window, and the loop repeats until the LLM decides no further tool calls are necessary.
Phase 4: Workflow Routing and State Transitions
If the agent is executing as part of a Mastra Workflow, the output of the agent is passed to the workflow engine. The engine evaluates transition conditions, updates the workflow's persistent state, and routes the payload to the next step in the DAG (e.g., another agent or a deterministic data transformation step).
Phase 5: Output Generation and Streaming
The final response is generated. In web applications, this is typically streamed back to the client in real-time. Mastra supports token-by-token streaming, allowing Next.js API routes to stream responses directly to frontend UI components, minimizing perceived latency.
Failure Paths and Resilience
If a tool execution fails (e.g., due to a third-party API timeout), Mastra catches the error, formats it as a system message, and feeds it back to the LLM, allowing the agent to attempt self-correction or fall back to an alternative strategy.
Architecture
The conceptual architecture of a Mastra-powered web application is designed for low-latency, serverless execution. The system starts at the client layer, typically a React or Next.js frontend, which sends an HTTP request to a Next.js API Route (the entry point). Inside the API Route, the Mastra Agent Runtime is instantiated. The Agent Runtime acts as the central orchestrator, managing the flow of data between three primary components: the LLM Provider, the Tool Registry, and the State Store.
When execution begins, the Agent Runtime queries the Tool Registry to fetch the schemas of all registered tools. It packages these schemas along with the user prompt and sends them to the LLM Provider. The LLM Provider returns either a direct text response or a tool call request. If a tool call is requested, the Agent Runtime executes the corresponding tool locally. The tool interacts with external databases or APIs, and its output is routed back through the Agent Runtime to the LLM Provider.
Throughout this execution loop, the Agent Runtime continuously writes state updates to the State Store (for session persistence) and emits trace data to an OpenTelemetry Collector. Once the LLM completes its reasoning, the Agent Runtime formats the final payload and streams it back through the Next.js API Route to the client, where execution ends.
Type-Safe Tool Definition with Zod
Mastra leverages Zod to enforce strict type safety at the boundary between the LLM and your application code. This is a critical departure from Python-based frameworks that rely on Pydantic; Mastra integrates directly with TypeScript's type inference engine. When you define a tool in Mastra, you provide a Zod schema for the input parameters. Mastra automatically infers the TypeScript types from this schema, ensuring that your execution handler is completely type-safe.
import { createTool } from '@mastra/core/agent';
import { z } from 'zod';
export const weatherTool = createTool({
id: 'get-weather',
description: 'Get the current weather for a specific location',
inputSchema: z.object({
location: z.string().describe('The city and state, e.g. San Francisco, CA'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
execute: async ({ input }) => {
// input is strongly typed as { location: string; unit: 'celsius' | 'fahrenheit' }
const response = await fetch(`https://api.weather.com/v1/${input.location}`);
if (!response.ok) throw new Error('Weather API failure');
const data = await response.json();
return { temp: data.temp, condition: data.condition };
},
});
During runtime, if the LLM attempts to call the tool with invalid arguments, Mastra's validation layer catches the schema violation before executing the handler. This prevents malformed payloads from causing unhandled exceptions in your application logic.
Orchestrating Workflows and State Machines
For complex, multi-step agentic tasks, relying on a single LLM reasoning loop can be non-deterministic and expensive. Mastra introduces a structured Workflow engine that allows developers to build deterministic state machines around their agents. Workflows are defined as a series of steps with explicit triggers, inputs, and transitions.
import { Workflow } from '@mastra/core/agent';
const leadGenWorkflow = new Workflow({
id: 'lead-generation',
});
leadGenWorkflow
.step('enrich-profile', {
execute: async ({ context }) => {
// Fetch data from Clearbit or LinkedIn
return { companySize: 150, industry: 'SaaS' };
},
})
.step('evaluate-lead', {
execute: async ({ context }) => {
const profile = context.steps['enrich-profile'].output;
// Run agentic evaluation
const isQualified = profile.companySize > 100 && profile.industry === 'SaaS';
return { isQualified };
},
})
.commit();
This workflow engine ensures that state is persisted between steps. If a step fails, the workflow can be resumed from the last successful checkpoint, preventing the need to re-run expensive LLM steps.
Serverless Deployment and Cold-Start Optimization
Deploying AI agents to serverless environments like Vercel or AWS Lambda requires careful optimization of bundle sizes and initialization times. Python runtimes often suffer from cold starts exceeding several seconds due to heavy ML libraries. Mastra is designed to be lightweight and tree-shakable.
To optimize Mastra for serverless deployment:
- Lazy Load LLM Providers: Avoid importing heavy SDKs at the global scope. Initialize your LLM clients inside the execution handler or use lightweight HTTP-based clients.
- Edge Runtime Compatibility: Where possible, configure your Next.js API routes to use the Edge Runtime. Mastra's core execution engine is compatible with V8 worker environments, which offer sub-millisecond cold starts.
- Externalize State: Use a serverless-compatible database (like Neon for PostgreSQL or Upstash for Redis) to persist agent memory and workflow state across ephemeral function invocations.
Code Example
import { Mastra, Agent, createTool } from '@mastra/core/agent';
import { z } from 'zod';
// Ensure environment variables are loaded
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('Missing OPENAI_API_KEY environment variable');
}
// Define a type-safe tool using Zod
const calculateLoanTool = createTool({
id: 'calculate-loan',
description: 'Calculates monthly mortgage payments based on principal, interest rate, and term.',
inputSchema: z.object({
principal: z.number().positive().describe('The loan amount'),
annualRate: z.number().positive().describe('Annual interest rate as a percentage (e.g., 5.5)'),
termYears: z.number().int().positive().describe('The term of the loan in years'),
}),
execute: async ({ input }) => {
const monthlyRate = input.annualRate / 12 / 100;
const numberOfPayments = input.termYears * 12;
const monthlyPayment =
(input.principal * monthlyRate * Math.pow(1 + monthlyRate, numberOfPayments)) /
(Math.pow(1 + monthlyRate, numberOfPayments) - 1);
return {
monthlyPayment: Math.round(monthlyPayment * 100) / 100,
totalPayments: Math.round(monthlyPayment * numberOfPayments * 100) / 100,
};
},
});
// Instantiate the Mastra Agent
const financialAgent = new Agent({
name: 'Financial Advisor',
instructions: 'You are a helpful financial assistant. Use the calculate-loan tool to provide accurate mortgage calculations.',
model: {
provider: 'OPENAI',
name: 'gpt-4o-mini',
apiKey: apiKey,
},
tools: {
calculateLoan: calculateLoanTool,
},
});
// Initialize Mastra with the agent
export const mastra = new Mastra({
agents: {
financialAgent,
},
});
// Example execution function
async function run() {
try {
const agentInstance = mastra.getAgent('financialAgent');
const response = await agentInstance.generate({
prompt: 'What would my monthly payment be for a $400,000 loan at 6% interest for 30 years?',
});
console.log('Agent Response:', response.text);
} catch (error) {
console.error('Execution failed:', error);
}
}
run();
Agent Response: Based on the calculations, your monthly payment for a $400,000 loan at an annual interest rate of 6% over a 30-year term would be $2,398.20. Over the life of the loan, your total payments would equal $863,352.15.
import { NextRequest, NextResponse } from 'next/server';
import { mastra } from './mastra-config'; // Path to the configured Mastra instance
export const runtime = 'edge'; // Use Edge Runtime for low latency
export async function POST(req: NextRequest) {
try {
const { prompt } = await req.json();
if (!prompt || typeof prompt !== 'string') {
return NextResponse.json({ error: 'Invalid prompt provided' }, { status: 400 });
}
const agent = mastra.getAgent('financialAgent');
// Initiate stream from Mastra agent
const stream = await agent.stream({
prompt,
});
// Return a streaming response
return new Response(stream.textStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
} catch (error: any) {
console.error('Streaming error:', error);
return NextResponse.json(
{ error: 'Internal Server Error', details: error.message },
{ status: 500 }
);
}
}
An HTTP 200 response streaming the text tokens of the agent's response in real-time.