Mastra is an open-source, TypeScript framework for building AI applications. It provides agents with memory, calling, workflows, and RAG capabilities. This guide uses Mastra v1.x.
In this guide, you’ll build an agent lets you read emails, send messages, and interact with Gmail and Slack using Arcade’s in a conversational interface with built-in authentication. You will also build a workflow that summarizes emails and sends them to Slack.
Outcomes
A Mastra and workflow that integrates Arcade for Gmail and Slack.
You will Learn
How to retrieve Arcade and convert them to Mastra format
How to create an with calling capabilities
How to create a workflow with multiple steps
How to handle Arcade’s authorization flow in your application
Before diving into the code, here are the key Mastra concepts you’ll use:
Mastra Studio : An interactive development environment for building and testing locally.
Zod schemas : Mastra uses Zod for type-safe definitions.
Memory : Persists conversation history across sessions using storage backends like LibSQL.
Processors : Transform messages before they reach the LLM. This tutorial uses:
ToolCallFilter: Removes tool calls and results from memory to prevent large API responses from bloating .
TokenLimiterProcessor: Limits input tokens to stay within model limits.
Build an agent
Create a new Mastra project
Terminal
npx create-mastra@latest arcade-agent
Select your preferred model provider when prompted (we recommend OpenAI). Enter your when asked.
Then navigate to the project directory and install the :
npm
Terminal
cd arcade-agentnpm install @arcadeai/arcadejs @ai-sdk/openai zod@3
pnpm
Terminal
cd arcade-agentpnpm add @arcadeai/arcadejs @ai-sdk/openai zod@3
yarn
Terminal
cd arcade-agentyarn add @arcadeai/arcadejs @ai-sdk/openai zod@3
We explicitly install zod@3 because the Arcade SDK’s toZodToolSet currently requires Zod 3.x. Zod 4 has a different internal API that isn’t yet supported.
The ARCADE_USER_ID is your app’s internal identifier for the (often the email you signed up with, a UUID, etc.). Arcade uses this to track authorizations per user.
Create the tool configuration
Create src/mastra//arcade.ts to handle Arcade tool fetching and conversion.
Handling large outputs: Tools like Gmail.ListEmails can return 200KB+ of email content. When this data is passed back to the LLM in the agentic loop, it can exceed token limits and cause rate limit errors. The code below includes output truncation to prevent this.
TypeScript
src/mastra/tools/arcade.ts
import { Arcade } from "@arcadeai/arcadejs";import { toZodToolSet, executeOrAuthorizeZodTool,} from "@arcadeai/arcadejs/lib";const config = { // Get all tools from these MCP servers mcpServers: ["Slack"], // Add specific individual tools individualTools: ["Gmail_ListEmails", "Gmail_SendEmail", "Gmail_WhoAmI"],};// Maximum characters for any string field in tool output// Keeps responses small while preserving structure (subjects, senders, snippets)const MAX_STRING_CHARS = 300;/** * Recursively truncates all large strings in objects/arrays. * This prevents token overflow when tool results are passed back to the LLM. */function truncateDeep(obj: unknown): unknown { if (obj === null || obj === undefined) return obj; if (typeof obj === "string") { if (obj.length > MAX_STRING_CHARS) { return obj.slice(0, MAX_STRING_CHARS) + "..."; } return obj; } if (Array.isArray(obj)) { return obj.map(truncateDeep); } if (typeof obj === "object") { const result: Record<string, unknown> = {}; for (const [key, value] of Object.entries(obj as Record<string, unknown>)) { result[key] = truncateDeep(value); } return result; } return obj;}export async function getArcadeTools(userId: string) { const arcade = new Arcade(); // Fetch tools from MCP servers const mcpTools = await Promise.all( config.mcpServers.map(async (server) => { const response = await arcade.tools.list({ toolkit: server }); return response.items; }) ); // Fetch individual tools const individualTools = await Promise.all( config.individualTools.map((toolName) => arcade.tools.get(toolName)) ); // Combine all tools const allTools = [...mcpTools.flat(), ...individualTools]; // Convert to Zod format for Mastra compatibility const zodTools = toZodToolSet({ tools: allTools, client: arcade, userId, executeFactory: executeOrAuthorizeZodTool, }); // Wrap tools with truncation and add 'id' property for Mastra Studio type ToolType = (typeof zodTools)[string] & { id: string }; const mastraTools: Record<string, ToolType> = {}; for (const [toolName, tool] of Object.entries(zodTools)) { const originalExecute = tool.execute; mastraTools[toolName] = { ...tool, id: toolName, execute: async (input: unknown) => { const result = await originalExecute(input); return truncateDeep(result) as Awaited<ReturnType<typeof originalExecute>>; }, } as ToolType; } return mastraTools;}
What this code does
Tool fetching
mcpServers: Fetches all tools from an server. Use this when you want everything a service offers (e.g., "Slack" gives you Slack_SendMessage, Slack_ListChannels, Slack_ListUsers, etc.)
individualTools: Fetches specific by name. Use this to cherry-pick only what you need (e.g., "Gmail_ListEmails" without Gmail_DeleteEmail or other tools you don’t want exposed)
There are a few reasons you might want to select your individually.
Security You may not want to expose all the a service offers, for instance Gmail_DeleteEmail is not necessary and could even be dangerous to expose to an designed to summarize emails.
Cost Each ’s schema consumes tokens. Loading all Gmail tools (~20 tools) uses significantly more tokens than loading just the 3 you need. This matters for rate limits and cost.
arcade.tools.list({ toolkit }): Fetches all tools from an server
arcade.tools.get(toolName): Fetches a single by its full name
toZodToolSet: Converts Arcade to Zod schemas that Mastra requires
executeOrAuthorizeZodTool: Handles and returns authorization URLs when needed
Output handling
truncateDeep: Recursively limits all strings to 300 characters to prevent token overflow when tool results are passed back to the LLM
Create the agent
Create src/mastra/agents/arcade.ts:
TypeScript
src/mastra/agents/arcade.ts
import { Agent } from "@mastra/core/agent";import { TokenLimiterProcessor, ToolCallFilter } from "@mastra/core/processors";import { Memory } from "@mastra/memory";import { LibSQLStore } from "@mastra/libsql";import { openai } from "@ai-sdk/openai";import { getArcadeTools } from "../tools/arcade";const userId = process.env.ARCADE_USER_ID || "default-user";// Fetch Arcade tools at startupconst arcadeTools = await getArcadeTools(userId);// Configure memory with conversation historyconst memory = new Memory({ storage: new LibSQLStore({ id: "arcade-agent-memory", url: "file:memory.db", }), options: { lastMessages: 10, },});export const arcadeAgent = new Agent({ id: "arcade-agent", name: "arcadeAgent", instructions: `You are a helpful assistant that can access Gmail and Slack.Always use the available tools to fulfill user requests.For Gmail:- Use Gmail_ListEmails to fetch recent emails- Use Gmail_SendEmail to send emails- Use Gmail_WhoAmI to get the user's email address- To find sent emails, use the query parameter with "in:sent"- To find received emails, use "in:inbox" or no query- When composing emails, use plain text (no markdown)For Slack:- Use Slack_SendMessage to send messages to channels or users- Use Slack_ListChannels to see available channelsAfter completing any action, always confirm what you did with specific details.IMPORTANT: When a tool returns an authorization response with a URL, tell the user to visit that URL to grant access. After they authorize, they can retry their request.`, model: openai("gpt-4o"), tools: arcadeTools, memory, // Filter out tool results from memory (they can be huge) and limit tokens inputProcessors: [new ToolCallFilter(), new TokenLimiterProcessor({ limit: 50000 })],});
Register the agent
Replace the contents of src/mastra/index.ts with the following to register your agent:
TypeScript
src/mastra/index.ts
import { Mastra } from "@mastra/core";import { arcadeAgent } from "./agents/arcade";export const mastra = new Mastra({ agents: { arcadeAgent, },});
Test with Mastra Studio
Start the development server:
npmpnpmyarn
npm
Terminal
npm run dev
pnpm
Terminal
pnpm dev
yarn
Terminal
yarn dev
Open http://localhost:4111 to access Mastra Studio. Select arcadeAgent from the list and try prompts like:
“Summarize my last 3 emails”
“Send a Slack DM to myself saying hello”
“What’s my Gmail address?”
On first use, the agent will return an authorization URL. Visit the URL to connect your Gmail or Slack account, then retry your request. Arcade remembers this authorization for future requests.
Build a workflow
Agents are great for open-ended conversations, but sometimes you want a deterministic process that runs the same way every time. Mastra workflows let you chain steps together, with each step’s output feeding into the next.
This workflow does the following:
Fetches emails from Gmail
Summarizes them with an LLM
Sends the digest as a direct message to the user on Slack
This also demonstrates how workflows:
handle large data the full email content stays internal to the workflow, and only the compact summary gets sent to Slack.
import { Mastra } from "@mastra/core";import { arcadeAgent } from "./agents/arcade";import { emailDigestWorkflow } from "./workflows/email-digest";export const mastra = new Mastra({ agents: { arcadeAgent, }, workflows: { emailDigestWorkflow, },});
Test the workflow
Restart the dev server and open Mastra Studio. In the sidebar, open Workflows. Select email-digest.
In the right sidebar, select “run” to run the workflow.
If authorization is required, the workflow returns an auth URL. Visit the URL, complete authorization, then run the workflow again.
Check your Slack DMs for the digest.
Key takeaways
Arcade tools work seamlessly with Mastra: Use toZodToolSet to convert Arcade tools to the Zod schema format Mastra expects.
Agent vs Workflow: The agent handles open-ended requests (“help me with my emails”). The workflow handles repeatable processes (“every morning, summarize and send to Slack”). Use both together for powerful automation.
Truncate large outputs: Tools like Gmail can return 200KB+ of data. Wrap tool execution with truncation to prevent token overflow in the agentic loop.
Authorization is automatic: The executeOrAuthorizeZodTool factory handles auth flows. When a tool needs authorization, it returns a URL for the user to visit.
Workflows need explicit auth handling: Unlike agents, workflows don’t have built-in auth handling. Catch 403 errors, call arcade.auth.start(), and pass the auth URL through your workflow steps.
Use agents for conversation, workflows for automation: Agents handle open-ended requests; workflows handle repeatable, deterministic processes.
Next steps
Add more tools: Browse the tool catalog and add tools for GitHub, Notion, Linear, and more.
Schedule your workflow: Use a cron job or Mastra’s scheduling to run your email digest every morning.
Deploy to production: Follow Mastra’s deployment guides to deploy your agent and workflows.
Building a multi-user app? This tutorial uses a single ARCADE_USER_ID for simplicity. For production apps where each user needs their own OAuth tokens, see Secure auth for production to learn how to dynamically pass user IDs and handle per-user authorization.
import { Agent } from "@mastra/core/agent";import { TokenLimiterProcessor, ToolCallFilter } from "@mastra/core/processors";import { Memory } from "@mastra/memory";import { LibSQLStore } from "@mastra/libsql";import { openai } from "@ai-sdk/openai";import { getArcadeTools } from "../tools/arcade";const userId = process.env.ARCADE_USER_ID || "default-user";const arcadeTools = await getArcadeTools(userId);const memory = new Memory({ storage: new LibSQLStore({ id: "arcade-agent-memory", url: "file:memory.db", }), options: { lastMessages: 10, },});export const arcadeAgent = new Agent({ id: "arcade-agent", name: "arcadeAgent", instructions: `You are a helpful assistant that can access Gmail and Slack.Always use the available tools to fulfill user requests.For Gmail:- Use Gmail_ListEmails to fetch recent emails- Use Gmail_SendEmail to send emails- Use Gmail_WhoAmI to get the user's email address- To find sent emails, use the query parameter with "in:sent"- To find received emails, use "in:inbox" or no query- When composing emails, use plain text (no markdown)For Slack:- Use Slack_SendMessage to send messages to channels or users- Use Slack_ListChannels to see available channelsAfter completing any action, always confirm what you did with specific details.IMPORTANT: When a tool returns an authorization response with a URL, tell the user to visit that URL to grant access. After they authorize, they can retry their request.`, model: openai("gpt-4o"), tools: arcadeTools, memory, // Filter out tool results from memory (they can be huge) and limit tokens inputProcessors: [new ToolCallFilter(), new TokenLimiterProcessor({ limit: 50000 })],});
src/mastra/index.ts (full file)
TypeScript
src/mastra/index.ts
import { Mastra } from "@mastra/core";import { arcadeAgent } from "./agents/arcade";import { emailDigestWorkflow } from "./workflows/email-digest";export const mastra = new Mastra({ agents: { arcadeAgent, }, workflows: { emailDigestWorkflow, },});