Skip to content

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 :

CoucheQui décideCe que tu contrôles
Orchestration (Mastra)Toisteps, branches, retry, persistance, concurrency, idempotence
Inférence (Anthropic SDK sous @ai-sdk/anthropic)Le providerchoix 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

bash
pnpm create mastra@latest my-app
# Choix: agents + workflows + rag, deploy: Cloudflare
cd my-app && pnpm dev   # playground sur http://localhost:4111

Agent minimal avec tool

ts
// 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

ts
// 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

ts
// 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èleID (alias, sans suffixe de date)Input / Output ($/M tok)ContexteQuand l'utiliser dans un agent Mastra
Opus 4.8 (flagship)claude-opus-4-85 / 251MOrchestrateur multi-agent, refacto long-horizon, raisonnement dur, code review
Sonnet 4.6 (mid)claude-sonnet-4-63 / 151MLe workhorse : SDR, RAG, tools standard. Défaut pour 80 % des steps
Haiku 4.5 (cheap)claude-haiku-4-51 / 5200KTriage, classification, sous-steps, extraction simple, fan-out parallèle

⚠️ À bannir à vue (stale/inventé) : Opus 4.7 comme 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.

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

ts
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_tokens dans un repo Mastra, c'est du code mort qui va 400 dès que le modèle passe en 4.7/4.8. Remplace par thinking: { 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 :

ts
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 manuel

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

  1. Prompt caching (cache_control sur 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.
  2. 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.
  3. effort plus 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.

ts
// 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

ts
// 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

ts
// 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 } },
});
bash
pnpm mastra deploy --target cloudflare
# → https://sdr.acme.fr/api/agents/sdrAgent/generate
# → https://sdr.acme.fr/api/workflows/sdrBatchWorkflow/start

Ce que ce pipeline montre :

  • Tools = simples async functions typées Zod (input/output enforced)
  • Memory semantic + working memory en 5 lignes
  • Workflow foreach avec concurrency: 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

ts
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 humaine

2. RAG en 10 lignes

ts
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)

ts
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

ts
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

ts
telemetry: { enabled: true, export: { type: "otlp", endpoint: "https://cloud.langfuse.com/api/public/otel" } }

🔄 Versions & écosystème 2026

PackageVersion mai 2026Note
@mastra/core1.4.xStable depuis v1.0 (janv. 2026)
@mastra/memory0.9.xWorking memory v2 (templates Markdown)
@mastra/rag0.8.xSemantic chunking + LLM-driven enrichment
@mastra/pg / @mastra/postgres0.7.xpgvector, transactions ACID
@mastra/deployer-cloudflare0.6.xCF Workers + Durable Objects state
@mastra/deployer-vercel0.6.xFluid compute optimized
@ai-sdk/*5.xVercel AI SDK v5 (breaking from v4)
Node≥ 20Bun 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

  1. Vercel AI SDK v4 vs v5 — Mastra 1.x requiert @ai-sdk/* ≥ 5. Upgrade path documentée mais casse les streamText callbacks v4. Verrouille tes versions.
  2. Workflow state persistence — par défaut in-memory. En prod, configure obligatoirement storage (Postgres/Redis/Cloudflare KV), sinon redémarrage = workflows perdus.
  3. Memory semanticRecall coûteux — chaque message embed un vecteur. Sur agent à 100 msgs/jour × 10 k users = 1 M embeddings/jour. Active uniquement quand pertinent et configure messageRange court.
  4. 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).
  5. Zod schemas trop laxistes = LLM remplit n'importe quoi. Mets .describe() sur chaque champ + .refine() pour les contraintes business.
  6. 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.
  7. mastra dev playground non-prod — gère pas auth, expose tout. Jamais déployé en public. C'est un outil de dev.
  8. Telemetry off par défaut → tu déploies aveugle. Active OTLP day 1.
  9. Pas de migration entre versions storage — si tu changes la version Postgres store, tu peux perdre les threads. Lock + snapshot avant upgrade.
  10. 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).
  11. 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.
  12. 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: 0 sur 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 panneSymptômeCause racineMitigation Mastra / Anthropic
Tool re-call après timeoutDouble création (2 emails, 2 deals Hubspot)LLM ne voit pas le résultat à temps, re-tenteidempotencyKey sur tools non-idempotents + dédup côté API tierce
Boucle infinie d'agentCoût qui explose, jamais end_turnTool qui échoue en boucle, schéma trop laxistemaxSteps sur l'agent + .refine() Zod + alerte sur step count p95
Refus du modèleRéponse vide / non conformeClassifier safety AnthropicVérifier stop_reason ; escalade humaine, pas retry
Workflow perdu au redémarrageReprise impossible, état évaporéstorage non configuré (in-memory par défaut)Postgres/Redis/CF KV obligatoire en prod
Dérive de coûtFacture ×3 sans hausse de traficOpus partout, cache cassé, thinking trop hautModèle par step + caching + effort + alerting sur tokens/user
Hallucination de tool argsTool appelé avec params absurdes.describe() manquants, schéma flouZod strict + .describe() par champ + eval faithfulness en CI
Latence p95 inacceptableStep qui dépasse 30 s (CF Workers)LLM calls séquentiels sur OpusParallé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 :

  1. resp.usage par appel (input/output/cache tokens) → coût et détection de dérive.
  2. Tool call success rate par tool → quel tool hallucine ou timeout.
  3. 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)

ComposantChoix recommandéCoût/mois (10k users actifs)
ComputeCloudflare Workers Paid5 $ + ~20 $ usage
Storage (Postgres+vect)Neon / Supabase Pro25-50 €
LLM (Claude Sonnet 4.6)80 % cached, ~500 calls/user/mo800-1500 €
Memory embeddings (OpenAI small)0,02 $/1M tokens30-80 €
TelemetryLangfuse Cloud29-99 €
TOTAL infra~1 000-1 800 €/mois

Pricing freelance senior sur Mastra

MissionDuréeTJMTotal
Audit + roadmap migration LangChain JS → Mastra4 j1 400 €5 600 €
Agent SDR end-to-end (cf. exemple)18-25 j1 300 €~28 000 €
Refonte assistant juridique RAG + memory + evals50 j1 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)

ts
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

ts
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

ts
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 ? »idempotencyKey dé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 usage par 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 baisser effort. Le caching et le bon modèle par step pèsent plus que tout micro-optim.

  • « Pourquoi budget_tokens ne 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

Bibliothèque tech perso — Achref