Mastra — Framework TypeScript full-stack pour agents IA
TL;DR — Mastra (par les créateurs de Gatsby) est le LangGraph du monde TypeScript : un framework opinionné pour agents, workflows (state machine + branching + suspend/resume), RAG (loaders + chunking + vector stores), memory (semantic + working), intégrations (Notion, Slack, Stripe, Hubspot, SendGrid, GitHub… 50+ via
@mastra/*-integration), evals (LLM-as-judge intégrés), et déploiement multi-cible (Cloudflare Workers, Vercel, Node, Bun). Premier framework AI à vraiment penser TS : types end-to-end, Zod partout, monorepo-friendly, dev playground inclus. Le choix par défaut 2026 si ton client est sur Next.js / Node monorepo. Stable v1.0 sorti début 2026, traction massive depuis (≈30k stars, intégré chez Vercel templates).
🧠 Mental model
┌────────────────────────────────────────┐
│ Mastra app │
│ (mastra.config.ts + src/mastra/*) │
└───────────┬────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────┐ ┌────────────┐ ┌──────────┐
│ Agents │ │ Workflows │ │ RAG │
│ ───────│ │ ──────────│ │ ────────│
│ tools │◄────────┤ steps │ │ loaders │
│ memory │ │ branches │ │ chunkers │
│ model │ │ suspend │ │ stores │
└────┬────┘ └─────┬──────┘ └────┬─────┘
│ │ │
└──────────┬─────────┴─────────┬───────────┘
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ Integrations │ │ Evals │
│ Slack/Notion │ │ judge / faith. │
│ Stripe/HS │ │ toxicity/PII │
└──────────────┘ └─────────────────┘
│
▼
┌──────────────────────────────┐
│ Deploy adapters │
│ CF Workers · Vercel · Node │
└──────────────────────────────┘Analogie — Mastra est à l'écosystème TS AI ce que Next.js est au frontend : opinionné, batteries-included, dev experience pensée pour shipper en production. Tu obtiens playground local au lancement, types stricts dans tout l'arbre, déploiement edge en 1 commande. Et comme Next.js, il a un "App Router" mental : agents, workflows, tools, integrations, evals — chacun son dossier, chacun son contrat.
Position 2026 :
- vs LangChain JS : moins de surface, plus stable, types corrects (LangChain JS reste un trainwreck typé).
- vs Vercel AI SDK : Vercel AI SDK = couche UI/streaming + génération. Mastra = orchestration. Ils se combinent très bien (Mastra côté backend, AI SDK côté React).
- vs LangGraph (Py) : équivalent fonctionnel mais natif TS. Si ton stack est TS, prends Mastra. Si Python, LangGraph.
- vs Pydantic AI : Pydantic AI = Python-only; Mastra = TS-only.
🧭 Le modèle mental du staff engineer : Mastra n'est pas le LLM
Erreur de junior n°1 : croire que Mastra « fait » l'agent. Mastra est un orchestrateur ; l'intelligence vient du provider (Anthropic, via @ai-sdk/anthropic). Mastra te donne la machine d'état autour du LLM : la boucle tool-call, la persistance, le retry, le branchement, le human-in-the-loop. Le LLM reste une boîte noire que tu appelles par HTTP.
Conséquence directe : toutes les décisions qui comptent vraiment en prod (coût, latence, qualité, refus) se jouent au niveau du modèle, pas du framework. Un staff engineer raisonne en deux couches séparées :
| Couche | Qui décide | Ce que tu contrôles |
|---|---|---|
| Orchestration (Mastra) | Toi | steps, branches, retry, persistance, concurrency, idempotence |
Inférence (Anthropic SDK sous @ai-sdk/anthropic) | Le provider | choix du modèle, effort, thinking adaptatif, prompt caching, gestion des refus |
Le piège : Mastra abstrait le provider derrière anthropic("claude-..."), donc tu crois ne pas avoir à connaître l'API Anthropic. Faux. Le jour où tu dois debugger un coût qui explose, un refus (stop_reason: "refusal"), un thinking qui ne se déclenche pas, ou un cache qui ne hit jamais, tu dois descendre à la couche SDK. Voir la section Choisir le modèle Anthropic plus bas.
🛠️ Code minimal
Bootstrap
pnpm create mastra@latest my-app
# Choix: agents + workflows + rag, deploy: Cloudflare
cd my-app && pnpm dev # playground sur http://localhost:4111Agent minimal avec tool
// src/mastra/agents/weather-agent.ts
import { Agent } from "@mastra/core/agent";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import { createTool } from "@mastra/core/tools";
const getWeather = createTool({
id: "get-weather",
description: "Fetch current weather for a city.",
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ tempC: z.number(), condition: z.string() }),
execute: async ({ context }) => {
const r = await fetch(`https://wttr.in/${context.city}?format=j1`);
const j = await r.json();
return {
tempC: Number(j.current_condition[0].temp_C),
condition: j.current_condition[0].weatherDesc[0].value,
};
},
});
export const weatherAgent = new Agent({
name: "Weather Agent",
instructions: "Reply concisely with temperature and condition.",
model: anthropic("claude-sonnet-4-6"),
tools: { getWeather },
});Workflow avec branching
// src/mastra/workflows/refund-workflow.ts
import { Workflow, Step } from "@mastra/core/workflows";
import { z } from "zod";
const triage = new Step({
id: "triage",
outputSchema: z.object({ category: z.enum(["refund", "delay", "other"]) }),
execute: async ({ context }) => ({ category: "refund" as const }),
});
const refund = new Step({ id: "refund", execute: async () => ({ ok: true }) });
const delay = new Step({ id: "delay", execute: async () => ({ ok: true }) });
const human = new Step({ id: "human", execute: async () => ({ escalated: true }) });
export const refundWorkflow = new Workflow({ name: "refund" })
.step(triage)
.then(refund, { when: { "triage.category": "refund" } })
.then(delay, { when: { "triage.category": "delay" } })
.then(human, { when: { "triage.category": "other" } })
.commit();Config Mastra
// src/mastra/index.ts
import { Mastra } from "@mastra/core";
import { weatherAgent } from "./agents/weather-agent";
import { refundWorkflow } from "./workflows/refund-workflow";
import { PostgresStore } from "@mastra/postgres";
export const mastra = new Mastra({
agents: { weatherAgent },
workflows: { refundWorkflow },
storage: new PostgresStore({ connectionString: process.env.DATABASE_URL! }),
telemetry: { enabled: true, export: { type: "otlp" } },
});🧬 Choisir le modèle Anthropic (la couche que Mastra n'abstrait pas pour toi)
Mastra route vers Anthropic via @ai-sdk/anthropic (anthropic("...")). Le string de modèle est ta décision la plus structurante — coût, latence et qualité en découlent. Facts canoniques 2026 :
| Modèle | ID (alias, sans suffixe de date) | Input / Output ($/M tok) | Contexte | Quand l'utiliser dans un agent Mastra |
|---|---|---|---|---|
| Opus 4.8 (flagship) | claude-opus-4-8 | 5 / 25 | 1M | Orchestrateur multi-agent, refacto long-horizon, raisonnement dur, code review |
| Sonnet 4.6 (mid) | claude-sonnet-4-6 | 3 / 15 | 1M | Le workhorse : SDR, RAG, tools standard. Défaut pour 80 % des steps |
| Haiku 4.5 (cheap) | claude-haiku-4-5 | 1 / 5 | 200K | Triage, classification, sous-steps, extraction simple, fan-out parallèle |
⚠️ À bannir à vue (stale/inventé) :
Opus 4.7comme flagship,claude-opus-4-7,claude-sonnet-4-7,claude-haiku-4-7, tout suffixe de date fabriqué (-20260101). Utilise les alias nus ci-dessus.
Pattern : modèle hétérogène par step (la vraie économie)
Le réflexe junior : mettre Opus partout « pour la qualité ». Le réflexe staff : le modèle est une décision par step, pas par agent. Dans le workflow SDR, le triage tourne sur Haiku, la rédaction sur Sonnet, l'escalade ambiguë sur Opus.
import { anthropic } from "@ai-sdk/anthropic";
const triageAgent = new Agent({
name: "Triage",
instructions: "Classe le lead en refund | delay | other. Réponds en un mot.",
model: anthropic("claude-haiku-4-5"), // cheap : c'est juste de la classification
});
const writerAgent = new Agent({
name: "Writer",
instructions: "Rédige l'email perso ICP-aware.",
model: anthropic("claude-sonnet-4-6"), // workhorse : qualité de rédaction suffisante
});
const escalationAgent = new Agent({
name: "Escalation",
instructions: "Cas litigieux : raisonne sur la jurisprudence et le contrat.",
model: anthropic("claude-opus-4-8"), // flagship : raisonnement dur, faible volume
});Thinking adaptatif & effort — ce que @ai-sdk/anthropic te laisse passer
La syntaxe budget_tokens (thinking: { type: "enabled", budget_tokens: N }) est supprimée sur Opus 4.7/4.8 et renvoie HTTP 400. Le bon réglage 2026 : thinking adaptatif (thinking: { type: "adaptive" }) + effort (low | medium | high) via output_config. Sonnet 4.6 et Haiku ne prennent pas de budget de thinking. Sous Mastra/AI SDK ces options passent par providerOptions.anthropic :
const reasoningAgent = new Agent({
name: "Hard reasoner",
instructions: "...",
model: anthropic("claude-opus-4-8"),
defaultGenerateOptions: {
providerOptions: {
anthropic: {
thinking: { type: "adaptive" }, // PAS budget_tokens (400 sur 4.7/4.8)
// effort passe via output_config côté SDK : low | medium | high
},
},
},
});Si tu vois
budget_tokensdans un repo Mastra, c'est du code mort qui va 400 dès que le modèle passe en 4.7/4.8. Remplace parthinking: { type: "adaptive" }.
Sorties structurées : laisse Zod faire, pas du prompt XML/JSON
Mastra pousse Zod partout — utilise-le pour les sorties structurées plutôt que de bricoler du prompting « réponds en JSON ». L'AI SDK mappe le schéma Zod vers la sortie structurée native du provider (output_config.format côté Anthropic), ce qui te garantit un objet valide ou une erreur typée — au lieu d'un JSON.parse() qui pète à 3 h du matin :
import { z } from "zod";
const result = await agent.generate("Extrait nom, email, plan", {
output: z.object({
name: z.string(),
email: z.string().email(),
plan: z.enum(["free", "pro", "enterprise"]),
}),
});
// result.object est typé ET validé — pas de parsing manuelLe coût réel d'un agent : où regarder
Le coût ne se lit pas dans Mastra, il se lit dans usage retourné par chaque appel LLM. Un staff engineer logge input/output/cache tokens par step vers l'OTLP, puis agrège par agent et par user. Trois leviers, par ordre d'impact :
- Prompt caching (
cache_controlsur le préfixe system + tools stable) — réduit l'input facturé de ~90 % sur la partie cachée. Le system prompt SDR (figé) et la liste de tools (déterministe) sont des candidats évidents. Le piège : tout octet volatil (timestamp, ID par requête) dans le préfixe casse le cache. - Le bon modèle par step (tableau ci-dessus) — Haiku au lieu d'Opus sur le triage, c'est 5× moins cher en input, 5× en output.
effortplus bas sur les tâches non critiques — moins de thinking = moins de tokens.
🎬 Cas d'usage concrets (France 2026)
1. Agent commercial B2B (SDR) — startup SaaS Paris, monorepo Next.js
Contexte — Scale-up SaaS HR-tech, 30 personnes, Next.js + tRPC + Postgres. Veulent agent SDR qui : (1) prend une liste de prospects (LinkedIn URL ou domaine), (2) enrichit (Apollo/Dropcontact), (3) génère email perso, (4) envoie via SendGrid + log Hubspot, (5) suit ouvertures et relance.
Pourquoi Mastra — Monorepo existant TS, pas envie d'ajouter Python service séparé. Vercel AI SDK insuffisant pour orchestrer plusieurs steps avec state + retry + Hubspot integration. LangChain JS = trop instable.
Mission — 25 j @ 1 350 € = 33 750 €.
2. Assistant juridique embed cabinet d'avocats (Lyon)
Contexte — Cabinet 12 avocats, app interne Remix/Next.js pour gestion dossiers. Veut "Copilot juridique" embed dans l'app : (1) recherche jurisprudence (Légifrance + base interne), (2) draft conclusions à partir template + dossier, (3) résume audiences (audio Whisper → résumé), (4) répond questions clients via chat.
Pourquoi Mastra — RAG built-in (loaders PDF/DOCX + chunking sémantique), memory long-terme par dossier, evals (faithfulness pour ne pas inventer une jurisprudence). Déploiement on-prem possible (binaire Node).
Mission — Forfait 65 k€ (3 mois) + maintenance 2 k€/mois.
3. Agent immobilier intégré CRM Hubspot (réseau franchisé)
Contexte — Réseau 80 agences immo, CRM Hubspot. Veulent agent qui : (1) écoute webhook Hubspot quand un lead arrive, (2) qualifie (budget, localisation, type bien), (3) match avec inventaire (Postgres + vector pour description "maison de charme"), (4) compose réponse email perso, (5) crée tâche dans Hubspot pour l'agent humain.
Pourquoi Mastra — Integration Hubspot officielle @mastra/hubspot, déploiement Cloudflare Workers (event-driven, scale auto), workflow suspend/resume si l'agent humain doit valider.
Mission — 30 j @ 1 200 € = 36 000 € + 5 € par lead traité (50 k leads/an = 250 k€ récurrent).
🛠️ Exemple end-to-end — Agent SDR Mastra (LinkedIn → email → Slack → Cloudflare)
Pipeline ~180 lignes. Agent SDR qui scrape LinkedIn (via Browserbase), enrichit, génère email perso ICP-aware, envoie SendGrid, log Slack, déploie Cloudflare Workers.
// src/mastra/agents/sdr-agent.ts
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { Memory } from "@mastra/memory";
import { PgVector } from "@mastra/pg";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
// ---------- Tools ----------
const scrapeLinkedIn = createTool({
id: "scrape-linkedin",
description: "Scrape LinkedIn profile via Browserbase.",
inputSchema: z.object({ profileUrl: z.string().url() }),
outputSchema: z.object({
fullName: z.string(),
title: z.string(),
company: z.string(),
companySize: z.string().optional(),
industry: z.string().optional(),
location: z.string().optional(),
recentPosts: z.array(z.string()).max(3),
}),
execute: async ({ context }) => {
const res = await fetch("https://api.browserbase.com/v1/scrape", {
method: "POST",
headers: {
"x-api-key": process.env.BROWSERBASE_API_KEY!,
"content-type": "application/json",
},
body: JSON.stringify({
url: context.profileUrl,
extractor: "linkedin-profile-v2",
}),
});
return res.json();
},
});
const enrichDropcontact = createTool({
id: "enrich-dropcontact",
description: "Find verified pro email from name + company.",
inputSchema: z.object({ firstName: z.string(), lastName: z.string(), company: z.string() }),
outputSchema: z.object({ email: z.string().email().nullable(), confidence: z.number() }),
execute: async ({ context }) => {
const r = await fetch("https://api.dropcontact.io/v1/enrich/all", {
method: "POST",
headers: { "X-Access-Token": process.env.DROPCONTACT_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({ data: [context] }),
});
const json = await r.json();
const first = json?.data?.[0];
return { email: first?.email ?? null, confidence: first?.email_qualification === "Valid" ? 0.95 : 0.3 };
},
});
const sendEmail = createTool({
id: "send-email",
description: "Send email via SendGrid.",
inputSchema: z.object({
to: z.string().email(),
subject: z.string().max(80),
body: z.string().max(2000),
}),
outputSchema: z.object({ messageId: z.string() }),
execute: async ({ context }) => {
const r = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SENDGRID_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
personalizations: [{ to: [{ email: context.to }] }],
from: { email: "[email protected]", name: "Sophie @ Acme" },
subject: context.subject,
content: [{ type: "text/plain", value: context.body }],
}),
});
return { messageId: r.headers.get("x-message-id") ?? "unknown" };
},
});
const slackLog = createTool({
id: "slack-log",
description: "Post a message to #sdr-activity Slack channel.",
inputSchema: z.object({ text: z.string() }),
outputSchema: z.object({ ok: z.boolean() }),
execute: async ({ context }) => {
const r = await fetch(process.env.SLACK_WEBHOOK!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: context.text }),
});
return { ok: r.ok };
},
});
// ---------- Memory ----------
const memory = new Memory({
storage: new PgVector({ connectionString: process.env.DATABASE_URL! }),
options: {
lastMessages: 20,
semanticRecall: { topK: 5, messageRange: 3 },
workingMemory: {
enabled: true,
template: `# Prospect
- Name:
- Company:
- ICP fit (1-5):
- Pain points:
- Last touch:
`,
},
},
});
// ---------- Agent ----------
export const sdrAgent = new Agent({
name: "SDR Agent",
instructions: `
Tu es Sophie, SDR senior chez Acme (SaaS HR-tech, ICP: PME 50-500 personnes France, RH/DAF).
Workflow:
1. scrape-linkedin du prospect
2. enrich-dropcontact (skip si confidence < 0.7)
3. compose email court (< 120 mots), 1 hook lié à un post LinkedIn récent OU à l'industrie,
1 valeur claire (gain RH chiffré), 1 CTA léger (15 min)
4. send-email
5. slack-log avec récap "✉️ sent to {name} ({company}) — subject: {subject}"
Règles dures:
- Jamais "j'espère que ce mail vous trouve bien"
- Pas plus de 3 phrases dans le body
- Toujours signer "Sophie | Acme HR-tech"
- Si pas d'email valide après enrich → slack-log "⚠️ no email for {name}" et STOP.
`,
model: anthropic("claude-sonnet-4-6"),
tools: { scrapeLinkedIn, enrichDropcontact, sendEmail, slackLog },
memory,
});Workflow batch
// src/mastra/workflows/sdr-batch.ts
import { Workflow, Step } from "@mastra/core/workflows";
import { z } from "zod";
import { sdrAgent } from "../agents/sdr-agent";
const processOne = new Step({
id: "process-prospect",
inputSchema: z.object({ profileUrl: z.string().url() }),
outputSchema: z.object({ status: z.enum(["sent", "skipped", "error"]), reason: z.string().optional() }),
execute: async ({ context }) => {
try {
const res = await sdrAgent.generate(
`Contacte ce prospect: ${context.profileUrl}`,
{ resourceId: context.profileUrl, threadId: `sdr-${Date.now()}` },
);
const text = res.text.toLowerCase();
if (text.includes("no email")) return { status: "skipped", reason: "no email" };
return { status: "sent" };
} catch (e: any) {
return { status: "error", reason: e.message };
}
},
});
export const sdrBatchWorkflow = new Workflow({
name: "sdr-batch",
triggerSchema: z.object({ profileUrls: z.array(z.string().url()).max(50) }),
})
.step(processOne)
.foreach({ items: "$.profileUrls", as: "profileUrl", step: processOne, concurrency: 3 })
.commit();Mastra root + Cloudflare deploy
// src/mastra/index.ts
import { Mastra } from "@mastra/core";
import { CloudflareDeployer } from "@mastra/deployer-cloudflare";
import { sdrAgent } from "./agents/sdr-agent";
import { sdrBatchWorkflow } from "./workflows/sdr-batch";
import { PostgresStore } from "@mastra/postgres";
export const mastra = new Mastra({
agents: { sdrAgent },
workflows: { sdrBatchWorkflow },
storage: new PostgresStore({ connectionString: process.env.DATABASE_URL! }),
deployer: new CloudflareDeployer({
accountId: process.env.CF_ACCOUNT_ID!,
workerName: "sdr-agent",
routes: [{ pattern: "sdr.acme.fr/*", zoneName: "acme.fr" }],
}),
telemetry: { enabled: true, export: { type: "otlp", endpoint: process.env.OTLP_URL } },
});pnpm mastra deploy --target cloudflare
# → https://sdr.acme.fr/api/agents/sdrAgent/generate
# → https://sdr.acme.fr/api/workflows/sdrBatchWorkflow/startCe que ce pipeline montre :
- Tools = simples async functions typées Zod (input/output enforced)
- Memory semantic + working memory en 5 lignes
- Workflow
foreachavecconcurrency: 3(gère le rate limiting) - Deploy Cloudflare = 1 commande, types préservés
resourceId+threadId= isolation conv par prospect
🎯 Patterns courants
1. Suspend / Resume pour human-in-the-loop
const draftStep = new Step({
id: "draft",
execute: async ({ context, suspend }) => {
const draft = await agent.generate(context.brief);
return suspend({ awaiting: "human-approval", draft });
},
});
const sendStep = new Step({
id: "send",
execute: async ({ context }) => sendgrid.send(context.draft),
});
new Workflow().step(draftStep).then(sendStep, { resumeWith: z.object({ approved: z.literal(true) }) });
// L'UI appelle workflow.resume({ runId, payload: { approved: true } }) après revue humaine2. RAG en 10 lignes
import { MDocument } from "@mastra/rag";
import { PgVector } from "@mastra/pg";
const doc = await MDocument.fromUrl("https://acme.fr/cgv.pdf");
const chunks = await doc.chunk({ strategy: "semantic", maxSize: 512 });
const vector = new PgVector({ connectionString: process.env.DATABASE_URL! });
await vector.upsert({ indexName: "cgv", vectors: chunks });
// puis dans le tool:
const results = await vector.query({ indexName: "cgv", queryText: q, topK: 5 });3. Multi-agent (handoff)
const triageAgent = new Agent({
...,
tools: { handoffToLegal, handoffToSales, handoffToSupport },
});handoffToX = tool qui appelle agentX.generate(...) et retourne sa réponse — pattern OpenAI Swarm-like.
4. Eval en CI
import { describe, it } from "vitest";
import { evaluate, faithfulness, toxicity } from "@mastra/evals";
describe("sdrAgent", () => {
it("stays on-brand and non-toxic", async () => {
const res = await sdrAgent.generate("Contact this prospect: ...");
const f = await evaluate(res, [faithfulness({ context }), toxicity()]);
expect(f.scores.faithfulness).toBeGreaterThan(0.8);
expect(f.scores.toxicity).toBeLessThan(0.1);
});
});5. Telemetry OpenTelemetry → Langfuse/Braintrust
telemetry: { enabled: true, export: { type: "otlp", endpoint: "https://cloud.langfuse.com/api/public/otel" } }🔄 Versions & écosystème 2026
| Package | Version mai 2026 | Note |
|---|---|---|
@mastra/core | 1.4.x | Stable depuis v1.0 (janv. 2026) |
@mastra/memory | 0.9.x | Working memory v2 (templates Markdown) |
@mastra/rag | 0.8.x | Semantic chunking + LLM-driven enrichment |
@mastra/pg / @mastra/postgres | 0.7.x | pgvector, transactions ACID |
@mastra/deployer-cloudflare | 0.6.x | CF Workers + Durable Objects state |
@mastra/deployer-vercel | 0.6.x | Fluid compute optimized |
@ai-sdk/* | 5.x | Vercel AI SDK v5 (breaking from v4) |
| Node | ≥ 20 | Bun supporté, Deno expérimental |
Tendances 2026 :
- Mastra + Convex combo monte fort (state + Mastra runtime).
- Mastra Cloud (managed) en private beta — alternative à LangSmith côté TS.
- MCP support natif (
@mastra/mcp) — tu connectes ton serveur MCP comme un tool. - Voice agents (
@mastra/voice) — wrap Whisper + ElevenLabs + agent en 5 lignes.
⚠️ Pitfalls
- Vercel AI SDK v4 vs v5 — Mastra 1.x requiert
@ai-sdk/*≥ 5. Upgrade path documentée mais casse lesstreamTextcallbacks v4. Verrouille tes versions. - Workflow state persistence — par défaut in-memory. En prod, configure obligatoirement
storage(Postgres/Redis/Cloudflare KV), sinon redémarrage = workflows perdus. - Memory
semanticRecallcoûteux — chaque message embed un vecteur. Sur agent à 100 msgs/jour × 10 k users = 1 M embeddings/jour. Active uniquement quand pertinent et configuremessageRangecourt. - Tools non-idempotents sans
idempotencyKey→ l'agent peut re-call après timeout et double-créer. Toujours passer une clé d'idempotence (ex:idempotencyKey: prospect.id + step). - Zod schemas trop laxistes = LLM remplit n'importe quoi. Mets
.describe()sur chaque champ +.refine()pour les contraintes business. - Cloudflare Workers : 30 s timeout — si ton agent fait 5 LLM calls séquentiels avec Claude Opus, tu dépasses. Soit batch parallèle, soit Durable Objects, soit deploy Node/Vercel.
mastra devplayground non-prod — gère pas auth, expose tout. Jamais déployé en public. C'est un outil de dev.- Telemetry off par défaut → tu déploies aveugle. Active OTLP day 1.
- Pas de migration entre versions storage — si tu changes la version Postgres store, tu peux perdre les threads. Lock + snapshot avant upgrade.
- Polyglot monorepo — si tu mixes Mastra (TS) et services Python, attention au format de tool args / outputs (Mastra utilise OpenAI tool schema, mais Pydantic/Python clients peuvent re-générer JSON différemment).
- Refus Anthropic non gérés — un appel peut revenir en
stop_reason: "refusal"(HTTP 200, pas une exception). Si ton step lit le texte de la réponse sans vérifier le stop reason, tu envoies un email vide ou tu plantes en aval. Traite le refus comme un cas explicite : log + escalade humaine, pas retry à l'identique. - Prompt caching cassé silencieusement — un
Date.now()ou un ID par requête injecté dans le system prompt invalide le cache à chaque appel.cache_read_input_tokens: 0sur des requêtes au préfixe identique = un invalidateur silencieux. Garde le préfixe (system + tools) byte-stable, mets le volatile en fin de messages.
🧨 Failure modes & raisonnement de prod (niveau staff)
Un agent en prod ne casse pas comme un CRUD. Voici les modes de panne qu'un staff engineer anticipe avant le premier déploiement, et comment Mastra (ou le provider) s'y prête.
| Mode de panne | Symptôme | Cause racine | Mitigation Mastra / Anthropic |
|---|---|---|---|
| Tool re-call après timeout | Double création (2 emails, 2 deals Hubspot) | LLM ne voit pas le résultat à temps, re-tente | idempotencyKey sur tools non-idempotents + dédup côté API tierce |
| Boucle infinie d'agent | Coût qui explose, jamais end_turn | Tool qui échoue en boucle, schéma trop laxiste | maxSteps sur l'agent + .refine() Zod + alerte sur step count p95 |
| Refus du modèle | Réponse vide / non conforme | Classifier safety Anthropic | Vérifier stop_reason ; escalade humaine, pas retry |
| Workflow perdu au redémarrage | Reprise impossible, état évaporé | storage non configuré (in-memory par défaut) | Postgres/Redis/CF KV obligatoire en prod |
| Dérive de coût | Facture ×3 sans hausse de trafic | Opus partout, cache cassé, thinking trop haut | Modèle par step + caching + effort + alerting sur tokens/user |
| Hallucination de tool args | Tool appelé avec params absurdes | .describe() manquants, schéma flou | Zod strict + .describe() par champ + eval faithfulness en CI |
| Latence p95 inacceptable | Step qui dépasse 30 s (CF Workers) | LLM calls séquentiels sur Opus | Parallélisme (asyncio.gather côté tool), streaming, Durable Objects |
Comment un staff raisonne sur l'observabilité : tu ne peux pas debugger ce que tu ne mesures pas. Les trois signaux non négociables, day 1, exportés OTLP → Langfuse/Braintrust :
resp.usagepar appel (input/output/cache tokens) → coût et détection de dérive.- Tool call success rate par tool → quel tool hallucine ou timeout.
- Workflow completion rate vs suspended/failed → où la machine d'état se bloque.
Sans ces trois-là, tu déploies aveugle. C'est la différence entre « ça marche en démo » et « ça tient 50 k leads/an ».
💰 Pricing / ROI client
Coût infra Mastra typique (production)
| Composant | Choix recommandé | Coût/mois (10k users actifs) |
|---|---|---|
| Compute | Cloudflare Workers Paid | 5 $ + ~20 $ usage |
| Storage (Postgres+vect) | Neon / Supabase Pro | 25-50 € |
| LLM (Claude Sonnet 4.6) | 80 % cached, ~500 calls/user/mo | 800-1500 € |
| Memory embeddings (OpenAI small) | 0,02 $/1M tokens | 30-80 € |
| Telemetry | Langfuse Cloud | 29-99 € |
| TOTAL infra | ~1 000-1 800 €/mois |
Pricing freelance senior sur Mastra
| Mission | Durée | TJM | Total |
|---|---|---|---|
| Audit + roadmap migration LangChain JS → Mastra | 4 j | 1 400 € | 5 600 € |
| Agent SDR end-to-end (cf. exemple) | 18-25 j | 1 300 € | ~28 000 € |
| Refonte assistant juridique RAG + memory + evals | 50 j | 1 400 € | 70 000 € |
| Forfait packagé "Agent Hubspot custom" | — | — | 25-45 k€ |
Pitch commercial : "Vous restez en TS, intégré à votre monorepo Next.js, déployable sur Cloudflare en 1 commande. Pas de service Python séparé à maintenir. Time-to-market : 3 semaines vs 8 semaines avec LangChain JS."
🧪 Testing / Eval
Tests d'agents (Vitest)
import { describe, it, expect } from "vitest";
import { sdrAgent } from "../src/mastra/agents/sdr-agent";
describe("sdrAgent", () => {
it("skips when no valid email found", async () => {
const res = await sdrAgent.generate("Contact: https://linkedin.com/in/fake-test", {
mocks: {
"enrich-dropcontact": { email: null, confidence: 0.1 },
},
});
expect(res.toolCalls.map(t => t.toolName)).toContain("slack-log");
expect(res.toolCalls.map(t => t.toolName)).not.toContain("send-email");
});
});Evals built-in
import { evaluate, faithfulness, contextualRelevance, completeness } from "@mastra/evals";
const result = await evaluate(agentResponse, [
faithfulness({ context: retrievedDocs }),
contextualRelevance({ query }),
completeness({ required: ["price", "delivery", "warranty"] }),
]);
// result.scores → { faithfulness: 0.92, contextualRelevance: 0.85, completeness: 1.0 }Workflow snapshot tests
const run = await sdrBatchWorkflow.createRun();
const result = await run.start({ triggerData: { profileUrls: [url1, url2] } });
expect(result.results["process-prospect-1"].output.status).toBe("sent");Métriques prod à monitorer (Mastra → OTLP → Langfuse)
- Tool call success rate (par tool)
- Workflow completion rate vs steps suspended/failed
- LLM latency p95 par agent
- Token usage par agent / par user
- Memory recall hit rate (% messages où semantic recall apporte réellement quelque chose)
🔁 Quand utiliser / éviter
✅ Utiliser Mastra
- Stack TypeScript (Next.js, Remix, NestJS, Bun…)
- Besoin d'orchestration multi-step (pas juste 1 LLM call)
- Tu veux types end-to-end sans 6 packages mal maintenus
- Déploiement edge / serverless (Cloudflare, Vercel)
- Intégrations SaaS standard (Slack, Notion, Hubspot, Stripe, GitHub…)
- Monorepo TS — partage des types entre frontend et agent
❌ Éviter Mastra
- Tu es Python-only → LangGraph / Pydantic AI / Agno
- POC < 1 jour, 1 LLM call simple → Vercel AI SDK seul suffit
- Besoin de fine-tuning / training maison → écosystème Python plus mature
- Recherche académique / papers à reproduire → LangChain / LlamaIndex / DSPy
- Stack ultra-spécifique (Java/Go/Rust) → SDK natif + orchestrator maison
Decision tree TypeScript AI
Stack TS ?
├─ 1 LLM call simple → Vercel AI SDK seul
├─ Agent + tools, pas de workflow → Vercel AI SDK + ai/tool
├─ Workflow / multi-agent / RAG → Mastra ★
├─ Convex / Liveblocks heavy stack → Convex + agents primitifs
└─ Legacy LangChain JS → migrer vers Mastra (3-5 sem)🏋️ Exercices
Progression du « implémente X » au « rends-le production-grade / casse-le puis répare-le / défends le chiffre ». Fais-les dans l'ordre — chacun construit sur le précédent.
Exercice 1 — Workflow refund avec suspend/resume réel
Objectif : construire un workflow refund où un remboursement > 500 € suspend pour validation humaine, puis reprend via workflow.resume().
Indice/Solution : reprends le draftStep / sendStep du pattern suspend/resume. Le step de décision retourne suspend({ awaiting: "human-approval", amount }) si amount > 500. Configure storage (PostgresStore) — sinon le runId est perdu au redémarrage et resume 404. Vérifie que l'état persisté contient bien le payload du suspend en interrogeant la table de Mastra.
Exercice 2 — Modèle hétérogène + budget de coût défendable
Objectif : router le workflow SDR sur trois modèles (claude-haiku-4-5 triage, claude-sonnet-4-6 rédaction, claude-opus-4-8 escalade) et produire un coût/lead chiffré et défendable.
Indice/Solution : logge resp.usage (input/output tokens) par step vers l'OTLP. Calcule le coût par lead = Σ (tokens_in × prix_in + tokens_out × prix_out) avec les prix canoniques (Haiku 1/5, Sonnet 3/15, Opus 5/25 $/M). Le piège du junior : oublier que l'escalade ne touche que ~5 % des leads — le coût Opus est amorti sur le volume. Défends le chiffre : « 0,012 € / lead, dont 70 % sur le triage Haiku ; passer le triage sur Sonnet le multiplierait par 3 sans gain mesurable ».
Exercice 3 — Prompt caching : fais hiter le cache, prouve-le
Objectif : activer le prompt caching sur le préfixe stable (system + tools) de l'agent SDR et prouver par usage que le cache hit.
Indice/Solution : mets cache_control: { type: "ephemeral" } sur le dernier bloc system. Envoie deux requêtes au préfixe identique. Sur la 2e, cache_read_input_tokens doit être > 0 et input_tokens doit chuter. Maintenant casse-le : injecte Date.now() dans les instructions. Re-mesure : cache_read_input_tokens retombe à 0 à chaque appel. Explique pourquoi (le caching est un prefix match — tout octet volatil en amont invalide tout l'aval) et où déplacer le timestamp (en fin de messages).
Exercice 4 — Casse l'idempotence, puis répare-la
Objectif : reproduire une double-création (2 emails SendGrid) en simulant un timeout LLM, puis garantir l'exactly-once.
Indice/Solution : ajoute un setTimeout artificiel dans sendEmail pour forcer le LLM à re-call. Observe les deux envois dans Slack. Répare : passe un idempotencyKey déterministe (prospect.id + "-send") et stocke les clés vues dans une table (ou Redis SETNX). Le tool ignore la 2e invocation avec la même clé. Vérifie qu'un re-run du workflow ne re-spam pas.
Exercice 5 — Gère le refus Anthropic de bout en bout
Objectif : rendre l'agent SDR robuste à stop_reason: "refusal" sans planter ni envoyer de contenu vide.
Indice/Solution : un refus arrive en HTTP 200, pas en exception — res.text peut être vide. Dans processOne, vérifie le stop reason avant de lire le texte. Si refus : slack-log "⚠️ refus modèle pour {url}" et return { status: "skipped", reason: "refusal" }. Bonus : ajoute une eval qui injecte un prospect au profil borderline et asserte que le workflow ne crashe pas et n'envoie aucun email.
Exercice 6 — Casse la latence sur Cloudflare Workers, puis tiens le SLA
Objectif : faire dépasser le timeout 30 s de CF Workers avec un agent à 5 LLM calls Opus séquentiels, puis ramener le p95 sous 30 s.
Indice/Solution : enchaîne 5 agent.generate Opus séquentiels — tu vas timeout. Trois réparations, à comparer : (1) paralléliser les tools indépendants (Promise.all / asyncio.gather côté tool), (2) descendre les steps non critiques sur Haiku/Sonnet + effort: "low", (3) basculer sur Durable Objects ou un déployeur Node/Vercel si la séquentialité est irréductible. Mesure le p95 avant/après via l'OTLP et défends ton choix d'architecture.
🎤 En entretien
Questions seniors que ce sujet attire, avec la réponse d'une ligne.
« Mastra vs LangGraph, comment tu choisis ? » — Par le stack : TS/monorepo Next.js → Mastra (types end-to-end, déploiement edge en 1 commande, pas de service Python à maintenir) ; Python → LangGraph. Le framework ne change pas la qualité de l'agent, qui vient du modèle.
« Tu mets quel modèle Anthropic sur un workflow multi-step ? » — Pas un seul : un modèle par step. Haiku 4.5 sur le triage/classification, Sonnet 4.6 comme workhorse de rédaction/RAG, Opus 4.8 réservé au raisonnement dur à faible volume — la décision de coût se prend par step, pas par agent.
« Comment tu garantis l'exactly-once quand un agent re-call un tool après timeout ? » —
idempotencyKeydéterministe (ex.entityId + step) côté tool + dédup persistée (Redis SETNX / contrainte unique en base) ; le LLM peut re-tenter, le tool ignore les invocations dupliquées.« Ton agent Mastra coûte 3× trop cher, par où tu commences ? » — Je logge
usagepar step : d'abord vérifier le prompt caching (cache_read_input_tokensà 0 = invalidateur silencieux dans le préfixe), puis re-router les steps surdimensionnés vers un modèle moins cher, puis baissereffort. Le caching et le bon modèle par step pèsent plus que tout micro-optim.« Pourquoi
budget_tokensne marche plus et tu mets quoi à la place ? » — Supprimé sur Opus 4.7/4.8 (renvoie 400) ; le bon réglage 2026 est le thinking adaptatif (thinking: { type: "adaptive" }) plus l'effort(low/medium/high) — Sonnet et Haiku, eux, ne prennent pas de budget de thinking.
🔗 Liens
- Docs officielles — https://mastra.ai/docs
- GitHub — https://github.com/mastra-ai/mastra
- Templates — https://github.com/mastra-ai/mastra/tree/main/examples
- Discord — actif, équipe core répond
- Mastra Cloud (beta) — https://cloud.mastra.ai
- Comparaison Mastra vs LangChain JS — https://mastra.ai/blog/why-mastra
- Vercel AI SDK v5 docs — https://sdk.vercel.ai
- Notre fiche
05-instructor.md— pour pure extraction structurée - Notre fiche
07-agno.md— équivalent Python si tu pivotes - Notre fiche
08-comparison-matrix.md— matrice complète frameworks 2026