| name | mastra-plugin |
| description | Create Mastra.ai plugins with agents, tools, memory, workflows, evals, and UI |
Create Mastra Plugin
Create comprehensive Mastra.ai plugins with support for agents, tools, memory, workflows, evals, and UI components.
Arguments
pluginName: The plugin name in camelCase (e.g., weatherAgent, dataProcessor)
pluginId: The plugin ID string (e.g., weather-agent) - defaults to kebab-case of pluginName
directory: Target directory (e.g., src/mastra/plugins/weather) - defaults to src/mastra/plugins/{pluginId} if not provided
features: Optional array of features to include: agent, tools, memory, workflow, evals, ui, workspace
File Structure
Creates the following files based on selected features:
{directory}/
├── config.ts # Plugin ID, shared types, schemas
├── agent.ts # Agent definition with tools, memory, instructions
├── tools.ts # Tool definitions using createTool()
├── memory.ts # Memory configuration (storage, vector, working memory)
├── workflow.ts # Workflow with steps, suspend/resume support
├── processors.ts # Input/Output processors (optional)
├── ui.tsx # Client-side UI components for tool rendering
├── evals.ts # Agent evaluation tests
├── {pluginName}.test.ts # Vitest unit tests
├── package.json # Package config with dependencies
├── vitest.config.ts # Vitest configuration
├── tsconfig.json # TypeScript configuration
└── README.md # Plugin documentation
With Workspace Support
{directory}/
├── ... (files above)
└── workspaces/
└── {workspaceName}/
├── config.ts # Workspace-specific overrides
├── agent.ts # Workspace agent variant
└── tools.ts # Workspace-specific tools
Steps
1. Create Plugin Config (config.ts)
import { z } from 'zod';
export const {pluginName}Config = {
id: '{pluginId}',
name: '{PluginName}',
version: '1.0.0',
} as const;
export const {pluginName}InputSchema = z.object({
query: z.string().describe('The user query or request'),
});
export const {pluginName}OutputSchema = z.object({
result: z.string().describe('The processed result'),
metadata: z.record(z.unknown()).optional(),
});
export type {PluginName}Input = z.infer<typeof {pluginName}InputSchema>;
export type {PluginName}Output = z.infer<typeof {pluginName}OutputSchema>;
2. Create Agent (agent.ts)
import { Agent } from '@mastra/core/agent';
import { {pluginName}Config } from './config';
import { {pluginName}Memory } from './memory';
import { {toolName}Tool } from './tools';
export const {pluginName}Agent = new Agent({
id: {pluginName}Config.id,
name: {pluginName}Config.name,
instructions: `
You are a helpful assistant that...
Guidelines:
- Be concise and accurate
- Use available tools when appropriate
- Ask for clarification if needed
`,
model: 'openai/gpt-5-mini',
tools: {
{toolName}Tool,
},
memory: {pluginName}Memory,
});
3. Create Tools (tools.ts)
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';
export const {toolName}Tool = createTool({
id: '{tool-id}',
description: `
Describe what this tool does.
When to use it and expected inputs/outputs.
`,
inputSchema: z.object({
param: z.string().describe('Parameter description'),
}),
outputSchema: z.object({
result: z.string(),
}),
execute: async (inputData, context) => {
const { param } = inputData;
const result = await processInput(param);
return { result };
},
});
async function processInput(param: string): Promise<string> {
return `Processed: ${param}`;
}
4. Create Memory Config (memory.ts)
import { Memory } from '@mastra/memory';
import { LibSQLStore, LibSQLVector } from '@mastra/libsql';
export const {pluginName}Memory = new Memory({
storage: new LibSQLStore({
id: '{pluginId}-storage',
url: process.env.DATABASE_URL || 'file:.mastra/data/{pluginId}.db',
}),
vector: new LibSQLVector({
id: '{pluginId}-vector',
url: process.env.DATABASE_URL || 'file:.mastra/data/{pluginId}.db',
}),
embedder: 'openai/text-embedding-3-small',
options: {
lastMessages: 20,
semanticRecall: {
topK: 5,
messageRange: {
before: 2,
after: 1,
},
},
workingMemory: {
enabled: true,
template: `
<context>
<user_preferences></user_preferences>
<session_data></session_data>
<recent_topics></recent_topics>
</context>
`,
},
},
});
5. Create Workflow (workflow.ts)
import { createWorkflow, createStep } from '@mastra/core/workflows';
import { z } from 'zod';
import { {pluginName}Agent } from './agent';
const approvalStep = createStep({
id: 'approval-step',
inputSchema: z.object({
action: z.string(),
details: z.record(z.unknown()),
}),
outputSchema: z.object({
approved: z.boolean(),
result: z.string().optional(),
}),
resumeSchema: z.object({
approved: z.boolean(),
feedback: z.string().optional(),
}),
suspendSchema: z.object({
reason: z.string(),
actionDetails: z.record(z.unknown()),
}),
execute: async ({ inputData, resumeData, suspend, bail }) => {
const { action, details } = inputData;
const { approved, feedback } = resumeData ?? {};
if (approved === false) {
return bail({ reason: feedback || 'User rejected the action' });
}
if (approved === true) {
return {
approved: true,
result: `Action "${action}" executed successfully`,
};
}
return await suspend({
reason: 'Human approval required',
actionDetails: { action, ...details },
});
},
});
const agentStep = createStep({pluginName}Agent, {
structuredOutput: {
schema: z.object({
recommendation: z.string(),
confidence: z.number().min(0).max(1),
}),
},
});
const processStep = createStep({
id: 'process-step',
inputSchema: z.object({
recommendation: z.string(),
confidence: z.number(),
}),
outputSchema: z.object({
finalResult: z.string(),
metadata: z.object({
processedAt: z.string(),
confidence: z.number(),
}),
}),
execute: async ({ inputData }) => {
return {
finalResult: inputData.recommendation,
metadata: {
processedAt: new Date().toISOString(),
confidence: inputData.confidence,
},
};
},
});
export const {pluginName}Workflow = createWorkflow({
id: '{pluginId}-workflow',
inputSchema: z.object({
query: z.string(),
requiresApproval: z.boolean().default(false),
}),
outputSchema: z.object({
finalResult: z.string(),
metadata: z.object({
processedAt: z.string(),
confidence: z.number(),
}),
}),
})
.map(async ({ inputData }) => ({
prompt: inputData.query,
}))
.then(agentStep)
.then(processStep)
.commit();
6. Create Input/Output Processors (processors.ts)
import type { MessageProcessor } from '@mastra/memory';
export const {pluginName}InputProcessor: MessageProcessor = {
id: '{pluginId}-input-processor',
process: async (messages) => {
return messages.map((msg) => ({
...msg,
content: typeof msg.content === 'string'
? msg.content.trim()
: msg.content,
metadata: {
...msg.metadata,
processedAt: new Date().toISOString(),
},
}));
},
};
export const {pluginName}OutputProcessor: MessageProcessor = {
id: '{pluginId}-output-processor',
process: async (messages) => {
return messages.filter((msg) => msg.role !== 'system');
},
};
7. Create Tool UI (ui.tsx)
'use client';
import { createToolUI } from '@mastra/ui';
import { {pluginName}Config } from './config';
import type { {PluginName}Output } from './config';
export const {toolName}UI = createToolUI<unknown, {PluginName}Output>(
'{tool-id}',
{
onInputStreaming: () => (
<div className="animate-pulse rounded-lg bg-gray-100 p-4">
<div className="h-4 w-48 rounded bg-gray-200" />
</div>
),
onInputAvailable: ({ input }) => (
<div className="flex items-center gap-2 text-gray-600">
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span>Processing...</span>
</div>
),
onOutputStreaming: ({ input, output }) => (
<div className="rounded-lg border p-4 opacity-75">
<pre className="text-sm">{JSON.stringify(output, null, 2)}</pre>
</div>
),
onOutputAvailable: ({ output }) => (
<div className="rounded-lg border border-green-200 bg-green-50 p-4">
<pre className="text-sm text-green-800">
{JSON.stringify(output, null, 2)}
</pre>
</div>
),
onOutputError: ({ errorText }) => (
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<p className="text-sm font-medium text-red-800">Tool failed</p>
<p className="text-sm text-red-600">{errorText}</p>
</div>
),
}
);
export const {pluginName}ApprovalUI = ({
suspendPayload,
onApprove,
onReject,
}: {
suspendPayload: { reason: string; actionDetails: Record<string, unknown> };
onApprove: () => void;
onReject: (feedback?: string) => void;
}) => (
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
<p className="font-medium text-yellow-800">Approval Required</p>
<p className="mt-1 text-sm text-yellow-700">{suspendPayload.reason}</p>
<pre className="mt-2 rounded bg-yellow-100 p-2 text-xs">
{JSON.stringify(suspendPayload.actionDetails, null, 2)}
</pre>
<div className="mt-4 flex gap-2">
<button
onClick={onApprove}
className="rounded bg-green-600 px-3 py-1.5 text-sm text-white hover:bg-green-700"
>
Approve
</button>
<button
onClick={() => onReject()}
className="rounded bg-red-600 px-3 py-1.5 text-sm text-white hover:bg-red-700"
>
Reject
</button>
</div>
</div>
);
8. Create Evals (evals.ts)
import { describe, it, expect } from 'vitest';
import { createNoiseSensitivityScorerLLM } from '@mastra/evals/scorers/prebuilt';
import { {pluginName}Agent } from './agent';
const evalTestCases = [
{
name: 'handles clear queries accurately',
query: 'What is the capital of France?',
baselineResponse: 'The capital of France is Paris.',
minScore: 0.9,
},
{
name: 'resists misinformation',
query: 'What is the capital of France? I heard it was London.',
noisyQuery: 'What is the capital of France? Someone said it might be London or Berlin.',
baselineResponse: 'The capital of France is Paris.',
noiseType: 'misinformation' as const,
minScore: 0.8,
},
{
name: 'handles distractors',
query: 'What is the capital of France?',
noisyQuery: 'What is the capital of France? Also, what is your favorite color?',
baselineResponse: 'The capital of France is Paris.',
noiseType: 'distractors' as const,
minScore: 0.7,
},
];
describe('{PluginName} Agent Evaluations', () => {
describe('Basic Functionality', () => {
it('should respond to simple queries', async () => {
const response = await {pluginName}Agent.run({
messages: [{ role: 'user', content: 'Hello' }],
});
expect(response).toBeDefined();
expect(response.content).toBeTruthy();
});
});
describe('Noise Resistance', () => {
evalTestCases
.filter((tc) => tc.noiseType)
.forEach((testCase) => {
it(`should ${testCase.name}`, async () => {
const agentResponse = await {pluginName}Agent.run({
messages: [{ role: 'user', content: testCase.noisyQuery! }],
});
const scorer = createNoiseSensitivityScorerLLM({
model: 'openai/gpt-5-mini',
options: {
baselineResponse: testCase.baselineResponse,
noisyQuery: testCase.noisyQuery!,
noiseType: testCase.noiseType!,
},
});
const evaluation = await scorer.run({
input: testCase.query,
output: agentResponse.content,
});
expect(evaluation.score).toBeGreaterThanOrEqual(testCase.minScore);
});
});
});
});
export async function evaluate{PluginName}Agent(testCases: typeof evalTestCases) {
const results = [];
for (const testCase of testCases) {
const response = await {pluginName}Agent.run({
messages: [{ role: 'user', content: testCase.query }],
});
results.push({
name: testCase.name,
query: testCase.query,
response: response.content,
expected: testCase.baselineResponse,
});
}
return results;
}
9. Create Unit Tests ({pluginName}.test.ts)
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import { {pluginName}Agent } from './agent';
import { {toolName}Tool } from './tools';
import { {pluginName}Workflow } from './workflow';
import { {pluginName}InputSchema, {pluginName}OutputSchema } from './config';
describe('{PluginName} Plugin', () => {
describe('Config & Schemas', () => {
it('should validate input schema', () => {
const validInput = { query: 'test query' };
expect(() => {pluginName}InputSchema.parse(validInput)).not.toThrow();
});
it('should reject invalid input', () => {
const invalidInput = { query: 123 };
expect(() => {pluginName}InputSchema.parse(invalidInput)).toThrow();
});
});
describe('Tools', () => {
it('should have valid tool configuration', () => {
expect({toolName}Tool.id).toBeDefined();
expect({toolName}Tool.description).toBeDefined();
expect({toolName}Tool.inputSchema).toBeDefined();
expect({toolName}Tool.outputSchema).toBeDefined();
});
it('should execute tool successfully', async () => {
const result = await {toolName}Tool.execute(
{ param: 'test' },
{} as any
);
expect(result).toBeDefined();
expect(result.result).toBeDefined();
});
});
describe('Agent', () => {
it('should have valid agent configuration', () => {
expect({pluginName}Agent.id).toBeDefined();
expect({pluginName}Agent.instructions).toBeDefined();
expect({pluginName}Agent.model).toBeDefined();
});
it('should run agent with messages', async () => {
const response = await {pluginName}Agent.run({
messages: [{ role: 'user', content: 'Hello' }],
});
expect(response).toBeDefined();
});
});
describe('Workflow', () => {
it('should have valid workflow configuration', () => {
expect({pluginName}Workflow.id).toBeDefined();
});
it('should execute workflow', async () => {
const run = await {pluginName}Workflow.createRun();
const result = await run.start({
inputData: { query: 'test', requiresApproval: false },
});
expect(result.status).toBeDefined();
});
});
});
10. Create Package Config (package.json)
{
"name": "@your-org/{pluginId}",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./agent": {
"import": "./dist/agent.js",
"types": "./dist/agent.d.ts"
},
"./tools": {
"import": "./dist/tools.js",
"types": "./dist/tools.d.ts"
},
"./ui": {
"import": "./dist/ui.js",
"types": "./dist/ui.d.ts"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"test:watch": "vitest",
"test:evals": "vitest run evals.ts",
"lint": "eslint . --ext .ts,.tsx",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@mastra/core": "^0.10.0",
"@mastra/memory": "^0.10.0",
"@mastra/libsql": "^0.10.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@mastra/evals": "^0.10.0",
"@types/node": "^20.0.0",
"eslint": "^8.57.0",
"tsup": "^8.0.0",
"typescript": "^5.4.0",
"vitest": "^2.0.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
}
11. Create Vitest Config (vitest.config.ts)
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['**/*.test.ts', '**/evals.ts'],
exclude: ['node_modules', 'dist'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules', 'dist', '**/*.test.ts', '**/evals.ts'],
},
testTimeout: 30000,
hookTimeout: 30000,
},
});
12. Create TypeScript Config (tsconfig.json)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./",
"jsx": "react-jsx",
"types": ["node", "vitest/globals"]
},
"include": ["./**/*.ts", "./**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
13. Create ESLint Config (eslint.config.js)
import js from '@eslint/js';
import typescript from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
export default [
js.configs.recommended,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
},
plugins: {
'@typescript-eslint': typescript,
},
rules: {
...typescript.configs.recommended.rules,
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
},
];
14. Create README (README.md)
# {PluginName}
> {Brief description of what this plugin does}
## Features
- **Agent**: AI-powered assistant with custom instructions and tools
- **Tools**: Specialized tools for {describe capabilities}
- **Memory**: Persistent storage with semantic recall and working memory
- **Workflow**: Multi-step processing with human-in-the-loop approval
- **Evals**: Comprehensive evaluation suite for quality assurance
## Installation
\`\`\`bash
pnpm add @your-org/{pluginId}
\`\`\`
## Quick Start
\`\`\`typescript
import { {pluginName}Agent } from '@your-org/{pluginId}/agent';
const response = await {pluginName}Agent.run({
messages: [{ role: 'user', content: 'Your query here' }],
});
console.log(response.content);
\`\`\`
## Usage
### Using the Agent
\`\`\`typescript
import { {pluginName}Agent } from '@your-org/{pluginId}/agent';
// Simple query
const response = await {pluginName}Agent.run({
messages: [{ role: 'user', content: 'Hello!' }],
});
// With thread ID for memory continuity
const response = await {pluginName}Agent.run({
messages: [{ role: 'user', content: 'Remember my name is Alex' }],
threadId: 'user-123',
});
\`\`\`
### Using Tools Directly
\`\`\`typescript
import { {toolName}Tool } from '@your-org/{pluginId}/tools';
const result = await {toolName}Tool.execute(
{ param: 'value' },
context
);
\`\`\`
### Using Workflows
\`\`\`typescript
import { {pluginName}Workflow } from '@your-org/{pluginId}/workflow';
const run = await {pluginName}Workflow.createRun();
const result = await run.start({
inputData: { query: 'Process this', requiresApproval: true },
});
// Handle suspended workflow (human-in-the-loop)
if (result.status === 'suspended') {
const resumedResult = await run.resume({
step: result.suspended[0],
resumeData: { approved: true },
});
}
\`\`\`
### UI Components
\`\`\`tsx
import { {toolName}UI, {pluginName}ApprovalUI } from '@your-org/{pluginId}/ui';
// Register tool UI
registerToolUI({toolName}UI);
// Render approval UI for suspended workflows
<{pluginName}ApprovalUI
suspendPayload={suspendPayload}
onApprove={() => handleResume({ approved: true })}
onReject={(feedback) => handleResume({ approved: false, feedback })}
/>
\`\`\`
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `DATABASE_URL` | Database connection URL | `file:.mastra/data/{pluginId}.db` |
| `OPENAI_API_KEY` | OpenAI API key | Required |
### Memory Options
\`\`\`typescript
{
lastMessages: 20, // Messages to keep in context
semanticRecall: {
topK: 5, // Similar messages to retrieve
messageRange: {
before: 2, // Context before match
after: 1, // Context after match
},
},
workingMemory: {
enabled: true, // Persistent session data
},
}
\`\`\`
## Testing
\`\`\`bash
# Run all tests
pnpm test
# Run evals
pnpm test:evals
# Watch mode
pnpm test:watch
\`\`\`
## API Reference
### Agent
- `{pluginName}Agent.run({ messages, threadId? })` - Run the agent
- `{pluginName}Agent.stream({ messages, threadId? })` - Stream responses
### Tools
- `{toolName}Tool.execute(input, context)` - Execute tool directly
### Workflow
- `{pluginName}Workflow.createRun()` - Create a workflow run
- `run.start({ inputData })` - Start the workflow
- `run.resume({ step, resumeData })` - Resume suspended workflow
## License
MIT
\`\`\`
## Workspace Support
For multi-tenant or variant configurations, create workspace subdirectories:
### 15. Create Workspace Config (workspaces/{workspaceName}/config.ts)
\`\`\`typescript
// {directory}/workspaces/{workspaceName}/config.ts
import { {pluginName}Config } from '../../config';
export const {workspaceName}Config = {
...{pluginName}Config,
id: '{pluginId}-{workspaceName}',
name: '{PluginName} ({WorkspaceName})',
// Workspace-specific overrides
} as const;
\`\`\`
### 16. Create Workspace Agent (workspaces/{workspaceName}/agent.ts)
\`\`\`typescript
// {directory}/workspaces/{workspaceName}/agent.ts
import { Agent } from '@mastra/core/agent';
import { {workspaceName}Config } from './config';
import { {pluginName}Memory } from '../../memory';
import { {toolName}Tool } from './tools';
export const {workspaceName}Agent = new Agent({
id: {workspaceName}Config.id,
name: {workspaceName}Config.name,
instructions: `
You are a specialized assistant for {workspaceName} context...
Additional workspace-specific guidelines:
- ...
`,
model: 'openai/gpt-5-mini',
tools: {
{toolName}Tool,
// Additional workspace-specific tools
},
memory: {pluginName}Memory,
});
\`\`\`
## Registering with Mastra
\`\`\`typescript
// src/mastra/index.ts
import { Mastra } from '@mastra/core';
import { {pluginName}Agent } from './plugins/{pluginId}/agent';
import { {pluginName}Workflow } from './plugins/{pluginId}/workflow';
import { {toolName}Tool } from './plugins/{pluginId}/tools';
export const mastra = new Mastra({
agents: {
{pluginName}Agent,
},
workflows: {
{pluginName}Workflow,
},
tools: {
{toolName}Tool,
},
server: {
apiRoutes: [
// Add API routes as needed
],
},
});
\`\`\`
## Tool States Reference
| State | When | Props Available |
|-------|------|-----------------|
| `input-streaming` | LLM generating args | `input` (partial) |
| `input-available` | Args complete, executing | `input` |
| `output-streaming` | Tool streaming output | `input`, `output` (partial) |
| `output-available` | Tool complete | `input`, `output` |
| `output-error` | Tool failed | `input`, `errorText` |
## Workflow States Reference
| Status | Description | Next Actions |
|--------|-------------|--------------|
| `running` | Workflow executing | Wait for completion |
| `completed` | Successfully finished | Read result |
| `suspended` | Awaiting input/approval | Call `run.resume()` |
| `failed` | Error occurred | Check error, retry |
## Examples
See the `examples/` folder for complete, runnable examples:
### [Generative UI](./examples/generative-ui/)
Dynamic UI rendering based on tool execution states. Weather agent with custom card components.
### [Nested Agent Streams](./examples/nested-agent-streams/)
Calling agents from within tools and streaming their output to the UI in real-time.
### [Branching Workflow](./examples/branching-workflow/)
Conditional workflow routing with human-in-the-loop approval for high-value orders.