Mastra vs LangGraph: el mismo agente TypeScript dos veces
Mastra vs LangGraph en TypeScript: construí el mismo agente con las dos. Grafo o workflow, aprobación humana, salida estructurada con Claude y cuándo elegir.

El contador de líneas decía 95 contra 142. Tenía el mismo agente funcionando con Mastra y con LangGraph, cada uno con sus tests en verde, y la tentación era evidente: titular «Mastra gana» y pasar a otra cosa.
No lo hice, porque el número no explicaba la diferencia que de verdad tuve que corregir. Era un 400 de la API de Anthropic que no venía de mi lógica, sino del método por defecto de una librería, que fuerza una llamada a herramienta que Claude Sonnet 5.5 rechaza.
Esa es la comparación Mastra vs LangGraph que me interesa: no cuál escribe menos, sino dónde te muerde cada una cuando construyes un agente que tiene que pararse, esperar a una persona y sobrevivir a un reinicio.
En corto: construí el mismo agente de triaje con LangGraph.js 1.5.3 y con Mastra 1.80.0. LangGraph te da un grafo de estados explícito con control fino de cada arista; Mastra te da un workflow de pasos tipados con Zod y más piezas incluidas. La elección depende de la forma del flujo: si tiene ciclos y ramas que quieres controlar, LangGraph.js; si es mayormente lineal y quieres Studio, memoria y model router incluidos, Mastra.
¿Qué es Mastra y qué es LangGraph?
LangGraph.js es una librería de orquestación de agentes en la que defines un grafo de estados: nodos que leen y escriben un estado compartido, y aristas, algunas condicionales, que deciden el siguiente paso. Es la versión TypeScript de LangGraph y se apoya en los modelos y las herramientas de LangChain.
Mastra es un framework de agentes TypeScript que reúne en una sola instancia agentes, herramientas, workflows de pasos encadenados, memoria, almacenamiento y un Studio local. Lo crearon developers que venían de Gatsby, como se presentaron en su Show HN, y está pensado para TypeScript desde el principio.
En la comparación Mastra vs LangGraph, las dos usan Zod, pausan para aprobación humana y guardan estado. La diferencia está en el modelo mental.
El agente que construí dos veces
Triaje recibe una incidencia de un usuario y hace cinco cosas: la clasifica por tipo y gravedad, investiga en la documentación con una herramienta de búsqueda con límite de vueltas, redacta un borrador, se pausa hasta que una persona lo aprueba, edita o rechaza, y publica lo aprobado.
Para que la comparación sea justa, las dos versiones comparten el paquete @curso/shared: esquemas Zod, instrucciones y prompt de redacción. Mismo modelo (Claude Sonnet 5.5) y versiones fijadas, verificadas el 29 de octubre de 2026.
Mastra vs LangGraph: grafo de estados o workflow de pasos
En LangGraph dibujas el flujo con aristas; en Mastra lo encadenas con .then(). Esa decisión se nota en todo lo demás.
Este es el grafo de Triaje en LangGraph.js. El bucle de investigación (research ⇄ tools) y la rama de rechazo son aristas que se ven en el código:
return new StateGraph(TriageState)
.addNode("classify", makeClassify(classifierModel))
.addNode("research", makeResearch(model))
.addNode("tools", runTools)
.addNode("writeDraft", makeDraft(model))
.addNode("approve", approve)
.addNode("publish", makePublish(tracker))
.addEdge(START, "classify")
.addEdge("classify", "research")
.addConditionalEdges("research", afterResearch, ["tools", "writeDraft"])
.addConditionalEdges("tools", afterTools, ["research", "writeDraft"])
.addEdge("writeDraft", "approve")
.addConditionalEdges("approve", afterApproval, ["publish", END])
.addEdge("publish", END)
.compile({ checkpointer });
Y este es el mismo flujo en Mastra:
return createWorkflow({ id: "triageWorkflow", inputSchema: Start, outputSchema: z.object({ published: z.boolean() }) })
.then(classify)
.then(researchAndDraft)
.then(approve)
.then(publish)
.commit();
Más corto, sí. Pero el bucle de investigación se ha ido dentro del paso researchAndDraft, donde el agente con la herramienta search hace sus vueltas con maxSteps. En LangGraph el bucle lo controlas tú. En Mastra lo controla el agente, y tú le pones un techo.
Es la misma pregunta de cuándo un grafo de estados gana al while loop.
El control explícito tiene un precio. En la revisión de código apareció un camino que ningún test cubría: si el límite de vueltas cortaba cuando el último mensaje pedía herramientas, la API recibía un tool_use sin su tool_result. Resultado: 400. La corrección fue aplicar el límite después de ejecutar las herramientas:
// El límite de vueltas se aplica DESPUÉS de ejecutar las herramientas.
const afterTools = (state: TriageStateValue) => (state.researchSteps >= MAX_RESEARCH_STEPS ? "writeDraft" : "research");
En un grafo, el protocolo de mensajes del modelo es problema tuyo.
¿Cómo se pausa un agente para aprobación humana en cada una?
LangGraph pausa con interrupt() y reanuda con new Command({ resume }); Mastra pausa con suspend() y reanuda con run.resume(). Las dos validan la respuesta humana con Zod.
En LangGraph, la aprobación es una sola llamada. El responseSchema valida la decisión al reanudar, y una respuesta inválida lanza ZodError sin romper el hilo:
export async function approve(state: Pick<TriageStateValue, "draft">): Promise<{ decision: Decision }> {
const decision = interrupt(state.draft, { responseSchema: DecisionSchema }) as Decision;
return { decision };
}
// Más tarde, en otro proceso:
await graph.invoke(new Command({ resume: decision }), { configurable: { thread_id: threadId } });
Necesita un checkpointer (MemorySaver, SqliteSaver o PostgresSaver) y un thread_id. Esa es la base de la persistencia de LangGraph.
En Mastra, el paso declara qué enseña al pausarse y qué espera al volver:
const approve = createStep({
id: "approve", inputSchema: Drafted, outputSchema: Decided, suspendSchema: DraftSchema, resumeSchema: DecisionSchema,
execute: async ({ inputData, resumeData, suspend }) => {
if (!resumeData) return await suspend(inputData.draft);
return { ...inputData, decision: resumeData };
},
});
// Más tarde:
const run = await mastra.getWorkflow("triageWorkflow").createRun({ runId });
await run.resume({ step: "approve", resumeData: decision });
La persistencia va en la instancia Mastra, con new LibSQLStore({ id, url }). Lo comprobé con otra instancia sobre el mismo fichero LibSQL: el workflow se reanudó con 0 llamadas al modelo. La doc oficial lo cuenta en Suspend and resume.
Las decisiones de diseño de esa pausa las cuento en arquitectura human-in-the-loop en TypeScript.
Salida estructurada con Claude: la diferencia que sí encontré
Es la diferencia de comportamiento entre las dos que tuve que corregir, y la comprueba un test. En LangChain, withStructuredOutput de @langchain/anthropic 1.6.0 usa por defecto el método functionCalling. Ese método fuerza tool_choice, y Claude Sonnet 5.5 responde 400.
La solución es pasar el método explícitamente:
const classifier = model.withStructuredOutput(ClassificationSchema, { name: "classification", method: "jsonSchema" });
Con jsonSchema la petición usa output_config.format. En Mastra no hubo que tocar nada:
const res = await agents.classifier.generate(text, { structuredOutput: { schema: ClassificationSchema } });
Con "anthropic/claude-sonnet-5-5" en el model router, Mastra ya envía output_config.format, sin tool_choice. Las dos versiones tienen un anthropic-wire.test.ts que intercepta fetch y comprueba lo que sale por la red. En LangGraph, quitar el method hace fallar el test.
Es un detalle de versión, no de diseño, pero solo lo ves construyendo lo mismo dos veces. Si los esquemas son la frontera de tu agente, merece la pena dominarlos: en el curso de Zod trabajo validación y transformación de datos en TypeScript, que es lo que sostiene cada paso de Triaje en las dos versiones.
¿Cuál tiene menos código? 95 contra 142 líneas
Mastra ocupa 95 líneas y LangGraph 142 en el núcleo del agente, contando sin comentarios ni líneas vacías. En LangGraph cuento graph.ts, state.ts y los cinco nodos; en Mastra, los cuatro ficheros de mastra/. La CLI que lo usa sale al revés: 63 en LangGraph, 68 en Mastra.
Ese dato no decide nada. Las 47 líneas de diferencia son, sobre todo, el estado explícito y el bucle de herramientas que en LangGraph escribes tú y en Mastra vive dentro del agente. Menos líneas significa menos control a la vista, no menos complejidad.
LangGraph.js vs Mastra: tabla comparativa
| Criterio | LangGraph.js 1.5.3 | Mastra 1.80.0 |
|---|---|---|
| Modelo mental | Grafo de estados: nodos, aristas condicionales, estado compartido con reducers | Workflow de pasos encadenados con .then(), cada uno con inputSchema/outputSchema Zod |
| Bucle de herramientas | Lo dibujas tú (research ⇄ tools) |
Dentro del agente, con maxSteps |
| Persistencia | Checkpointer (MemorySaver, SqliteSaver, PostgresSaver) + thread_id |
storage en la instancia Mastra (p. ej. LibSQLStore) + runId |
| Human-in-the-loop | interrupt(valor, { responseSchema }) y new Command({ resume }) |
suspendSchema/resumeSchema, suspend() y run.resume({ step, resumeData }) |
| Salida estructurada con Claude Sonnet 5.5 | Hay que pasar method: "jsonSchema"; el método por defecto da 400 |
Funciona tal cual con structuredOutput: { schema } |
| Modelo | new ChatAnthropic({ model }) |
Model router: "anthropic/claude-sonnet-5-5" |
| Tooling | Streaming por modos (updates, messages, custom), getState, time-travel |
Studio local con mastra dev en localhost:4111 |
| Tests | fakeModel() de langchain, documentado |
MockLanguageModelV4 de ai/test, que Mastra no documenta |
| Núcleo de Triaje | 142 líneas | 95 líneas |
| Limitación o riesgo | La doc va por delante del paquete; el protocolo tool_use/tool_result es responsabilidad tuya |
Más opinado: salirte de sus patrones cuesta; los mocks de test pueden romperse al subir versión |
La última fila es la que más pesa. En LangGraph, los interrupts tipados de las notas de la v1 (runtime.interrupt.nombre()) no existen en 1.5.3: fallan en ejecución con «runtime.interrupt.approval is not a function». Lo que sí funciona es interrupt(valor, { responseSchema }).
En Mastra hay tres cosas que no vienen en la portada. Studio se ejecuta desde .mastra/, así que las rutas de LibSQL tienen que ser absolutas. Una herramienta como objeto plano, sin createTool, falla en silencio. Y un agente usado como paso del workflow llama al modelo en streaming (doStream): tu mock tiene que cubrir los dos modos.
¿Qué dicen otros developers de Mastra frente a LangGraph?
En el debate público, la crítica concreta a Mastra es la que anticipa la tabla: es un framework opinado, rápido mientras encajas en sus patrones y costoso cuando no.
En el Show HN de Mastra 1.0 de enero de 2026, un usuario contó una experiencia que encaja con la última fila de la tabla. Escribió que «Mastra is by design batteries included and opinionated» y que, cuando no encajas en un patrón existente, cuesta seguir siendo productivo.
Contó que habían tenido que crear sus propias rutas de Hono y que alguien de su equipo estaba reconstruyéndolo todo con LangGraph/LangChain.
La respuesta del equipo de Mastra en el mismo hilo fue que habían aprendido que los usuarios necesitan poder «eject and adapt Mastra however they want», y que habían añadido adaptadores de servidor y almacenamiento más configurable.
Las dos cosas son ciertas a la vez. Las baterías incluidas te aceleran mientras tu problema se parece al que imaginó el framework.
¿Cuándo elegir LangGraph.js y cuándo Mastra?
Elige LangGraph.js si el flujo tiene ciclos, ramas y reintentos que quieres ver y testear uno a uno. Elige Mastra si tu equipo es solo TypeScript, el flujo es mayormente lineal y quieres agentes, workflows, memoria y Studio en una sola pieza.
LangGraph pesa más si necesitas subgrafos, paralelo dinámico con Send, o recorrer el historial con getStateHistory para hacer replay desde un checkpoint o fork con updateState, o si ya usas LangChain. Mastra pesa más si quieres un Studio local sin montar nada y un model router con strings proveedor/modelo en vez de configurar el SDK de cada proveedor.
Si dudas, escribe antes la especificación del agente: qué pasos tiene, dónde se pausa y qué pasa si una respuesta humana no es válida. La elección suele salir sola. Es el método que explico en el libro de Spec-Driven Development.
Límites de esta comparación
Esta comparación Mastra vs LangGraph es un caso, no un benchmark. Conviene leerlo con estas limitaciones:
- Un solo agente. Triaje es casi lineal. Un sistema multiagente o con mucho paralelismo puede inclinar la balanza.
- Una sola versión de cada una. LangGraph.js 1.5.3 y Mastra 1.80.0, a 29 de octubre de 2026. La trampa de salida estructurada depende de
@langchain/anthropic1.6.0 y puede desaparecer en la siguiente. Al publicar ya hay LangGraph.js 1.6.0 y Mastra 1.81.0; no he repetido las pruebas con ellas. - Un solo modelo. Claude Sonnet 5.5. Con otro proveedor, el comportamiento de
withStructuredOutputy del model router puede ser distinto. - Sin cifras de coste, latencia ni calidad que sirvan para decidir. Hay un arnés
comparar/que ejecuta las dos versiones en serie sobre las mismas incidencias y mide llamadas, tokens, milisegundos y errores, y una pasada de evals con 20 incidencias sintéticas. Una sola pasada no demuestra que un framework sea mejor, así que no doy números. - Sin producción real. Probé persistencia, reinicios y Docker en local, no con tráfico de usuarios.
Un riesgo que vale para las dos: fija versiones. En junio de 2026 se publicaron versiones comprometidas de varios paquetes @mastra en npm; el aviso en GitHub se cerró en unas horas, pero un framework de agentes es una dependencia de la cadena de suministro como cualquier otra.
¿Dónde se construye Triaje con las dos?
Estoy preparando un curso de agentes en TypeScript con LangGraph.js y Mastra. Triaje se construye primero con LangGraph.js y después con Mastra, y una sección los compara cara a cara con el arnés y los tests de este post.
Todavía no tiene fecha.
Lo que puedes hacer hoy
Antes de elegir framework, dibuja tu agente en papel. Si el dibujo tiene flechas que vuelven hacia atrás y quieres controlarlas tú, empieza por LangGraph.js. Si es una fila de cajas con entradas y salidas, empieza por Mastra, y tienes un punto de partida en el tutorial para crear un agente TypeScript con Mastra.
Si quieres la base que hay debajo de los dos frameworks (herramientas, memoria, RAG, MCP y multiagente), descarga gratis el ebook El Developer Agéntico. Y si es tu primer agente, empieza por cómo construir un agente de IA desde cero.
Preguntas frecuentes
¿LangGraph.js vs Mastra: cuál es mejor framework de agentes TypeScript?
Ninguna es mejor en abstracto: con el mismo agente, las dos resolvieron clasificación, búsqueda, aprobación humana y persistencia. LangGraph.js da más control sobre ciclos y estado; Mastra, más piezas integradas.
¿Se puede pausar un agente para aprobación humana en Mastra y en LangGraph?
Sí. LangGraph usa interrupt() con un checkpointer y new Command({ resume }); Mastra usa suspend() y run.resume({ step, resumeData }). En las dos, la respuesta humana se valida con Zod.
¿Por qué falla withStructuredOutput con Claude Sonnet 5.5 en LangGraph?
Porque el método por defecto de @langchain/anthropic 1.6.0 es functionCalling, que fuerza tool_choice, y Sonnet 5.5 lo rechaza con un 400. Se arregla pasando method: "jsonSchema", que envía output_config.format. En Mastra, structuredOutput: { schema } funciona sin cambios.
¿Se puede testear un agente de Mastra sin llamar a la API?
Sí, con MockLanguageModelV4 de ai/test pasado como model del agente. Funciona con Mastra 1.80.0 y ai 7.0.143, pero Mastra no documenta este uso, así que puede romperse al subir de versión. En LangGraph, fakeModel() de langchain sí está documentado.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Quieres construir con agentes en serio?
Curso «Construye con IA» con Claude Code, de la idea al producto.
Developer con más de 15 años en frontend y creador de Dominicode. Escribe sobre IA aplicada al desarrollo de software.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.