# Building Agents with Mastra

Source: https://www.omnara.com/blog/building-agents-with-mastra
Author: Kartik Sarangmath
Published: 2026-11-09

[Mastra](https://mastra.ai) is an open-source TypeScript framework for building AI agents. You define agents, tools, workflows, and memory in code, and Mastra runs the agent loop. Studio, its local app, lets you chat with your agents and read their traces, so a first agent works on your laptop quickly.

Mastra's main draw is flexibility, because the agent is your own code with hooks at every step. Agents built this way still end up with the same shape: a loop, tools, memory, approvals, subagents, and a machine to run commands on. Most of the code that differs between agents configures those parts, and the code that stays the same becomes a platform you have to run.

## What Mastra Is

You run Mastra agents inside your own app or on a server Mastra generates with `mastra build`. An agent is a class with instructions, a model, and tools:

```ts
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { orders } from "../db";

const getOrder = createTool({
  id: "get-order",
  description: "Look up an order by its ID",
  inputSchema: z.object({ orderId: z.string() }),
  execute: async ({ orderId }) => orders.find(orderId),
});

export const support = new Agent({
  id: "support",
  name: "Support",
  instructions: "You help customers with their orders.",
  model: "anthropic/claude-sonnet-5-5",
  tools: { getOrder },
});
```

Mastra also has workflows for steps you want to control yourself, several kinds of memory, evals, tracing, MCP, and sandboxes for running commands.

## Every Agent Has the Same Shape

Every agent runs the same loop: call the model, run the tools it asked for, and decide whether to go again. That's true of Claude Code, Codex, Mastra, and Omnara alike, and it's why [the harness matters less than it looks](https://www.omnara.com/blog/the-harness-doesnt-matter). Mastra's hooks change what goes in and comes out at each step, but not the order of the steps or the message format.

What changes from one agent to the next is a short list: the prompt, the tools, the model, memory, which tools need approval, which subagents it can call, and where it runs commands. Mastra exposes each of these as code. A managed agent platform exposes the same things as configuration:

| Mastra customization | What it's for | On Omnara |
| --- | --- | --- |
| `instructions`, `model`, and `tools` set per request | Different behavior per customer or task | An agent per customer with its own config, [changed mid-conversation](https://docs.omnara.com/events/sending-input#change-the-config-mid-conversation) through the API |
| `requireToolApproval` and `beforeToolCall` | Gate risky actions | [Per-tool permissions](https://docs.omnara.com/tools/permissions): run, ask first, or block |
| `createTool` with your own code | Reach your systems | [Custom tools](https://docs.omnara.com/tools/custom) in your backend, in any language |
| `agents` on a supervisor | Delegate work | [Subagents](https://docs.omnara.com/tools/built-in#subagents), each on any lab's model |
| `memory` | Remember across conversations | Memory stores shared across conversations, plus the full event history |
| `workspace` with a sandbox | Run commands | Your own computers or [sandbox providers](https://docs.omnara.com/machines/providers/overview), several at once |
| MCP clients and skills | Add capabilities | [MCP servers](https://docs.omnara.com/tools/mcp) and skills in the config |

Two things in Mastra have no config equivalent. One is code inside the loop, such as rewriting the prompt partway through a turn, a custom stop condition, or a check on every model response. The other is the workflow engine, with branches, parallel steps, and pauses. Most of that logic can move into a tool, a permission rule, or the prompt, so check before you build around it.

Code inside the loop also has a cost in production. It lives in the process, so a crash or a deploy can lose it. Mastra's docs say delegation hooks passed per call "are closures and can't be restored in a new process," so a recovered run falls back to default behavior. In-process flexibility and durability pull against each other.

The flexibility runs the other way too. Mastra tools are TypeScript, while Omnara's custom tools run in your backend in any language. Mastra agents defined in code change with a deploy, and [stored agents](https://mastra.ai/reference/client-js/agents#stored-agents) can't add a tool without one, while an Omnara agent takes a new model, tools, permissions, or instructions while it runs.

## Everyone Builds the Same Platform

Every agent in production also needs the same things around the loop. It needs state that survives crashes, a sandbox, credentials kept away from the model, approvals that can wait, streams that reconnect, access control, and a way to run on more than one server. Teams that build on Mastra end up building these pieces, and they look alike from team to team.

Mastra's own customer stories show it. MongoDB describes [“a wrapper around Mastra that handles the internal plumbing — auth, networking, and exposing agents over HTTP”](https://mastra.ai/customers/mongodb) for its 20-plus engineering teams. Salesforce, whose coding IDE for about 100,000 developers runs a Mastra harness for non-Claude models, [wrote five separate implementations](https://mastra.ai/customers/salesforce) to reach model providers. It also built its own mechanisms for pausing on approvals and for holding a run open while an outside tool finishes.

Mastra is building toward the same shape. Stored agents keep agent configs in a database and edit them at runtime. Agent Builder manages them in the browser, and Mastra's hosted platform runs the server for you. That's a managed agent platform taking form inside a framework.

This is why managed agent platforms exist. Claude Managed Agents, OpenAI's Agents API, and Omnara each run the loop and the pieces around it as a service, because every production agent needs the same ones. [What AI Agents Need in Production](https://www.omnara.com/guides/ai-agent-infrastructure) compares them.

## Running Mastra in Production

If you run Mastra yourself, a deployment that survives restarts and runs on more than one server needs:

- **Postgres or another persistent store**, since the default store [loses data when the process exits](https://mastra.ai/docs/storage).
- **An auth provider**, since with none configured, [every route and Studio are public](https://mastra.ai/docs/auth/overview). Per-user thread ownership is a separate setting, and built-in roles need Mastra's paid enterprise license.
- **A remote sandbox** such as E2B or Daytona, because the local sandbox runs commands on the server itself.
- **A longer drain window**, because a deploy otherwise cuts off any turn still running after five seconds.
- **Durable agents with crash recovery turned on**, because a plain agent run can't resume after its process exits, and recovery is off by default.
- **Redis**, for a message bus that every replica shares and a cache that replays streams after a reconnect.
- **Inngest**, which Mastra's docs name for production durable agents. It's a separate service you run or pay for.

Here's that setup in one config:

```ts
import { Mastra } from "@mastra/core";
import { createDurableAgent } from "@mastra/core/agent/durable";
import { PostgresStore } from "@mastra/pg";
import { MastraJwtAuth } from "@mastra/auth";
import { RedisStreamsPubSub } from "@mastra/redis-streams";
import { RedisServerCache } from "@mastra/redis";
import Redis from "ioredis";

const durableSupport = createDurableAgent({
  agent: support, // with a Workspace whose sandbox is an E2BSandbox
  cache: new RedisServerCache({ client: new Redis(process.env.REDIS_URL) }),
});

export const mastra = new Mastra({
  agents: { durableSupport },
  storage: new PostgresStore({ id: "mastra-storage", connectionString: process.env.DATABASE_URL }),
  pubsub: new RedisStreamsPubSub({ url: process.env.REDIS_URL }),
  recovery: { durableAgents: "auto" },
  server: {
    auth: new MastraJwtAuth({ secret: process.env.MASTRA_JWT_SECRET }),
    drainTimeout: 600_000, // let a turn finish for up to 10 minutes on deploy
    timeout: 600_000, // requests otherwise get a 504 after 3 minutes
  },
});
```

Durable agents and the workers that split processing across processes are both in beta, where breaking changes can ship in minor releases. Workers have no dead-letter queue, and only one scheduler process can run without firing schedules twice.

Mastra's hosted platform runs the server through `mastra deploy`. Keeping a server up around the clock is a $100-per-project add-on, on top of CPU time, and models bought through Mastra's gateway cost 5.5% over the market rate. The durable agent setup, storage, and upgrades still live in your code.

## What Breaks in Production

Most of the hard problems come from state that lives in one process while the work moves between processes. Mastra's issue tracker shows each one, and Mastra has fixed several of them within weeks.

**Deploys interrupting runs.** In July, a team running about 28 production agents on Slack [reported](https://github.com/mastra-ai/mastra/issues/19056) that "every deploy restarts the process, and any agent run that is in-flight at that moment dies silently." Mastra added boot-time recovery for durable agents days later. It's still opt-in, so an agent built with the defaults behaves the same way today.

**Recovery repeating side effects.** Mastra's docs say recovery "can reissue LLM calls (with real cost) and re-execute tool calls." Workflow runs hold a lease while they execute, but the docs call it "advisory" and say it "doesn't fence writes," so recovery can run a step a second time. An [open issue](https://github.com/mastra-ai/mastra/issues/28712) from October describes a rollout where about 5,400 stale runs "re-executed each run once per booting replica, which duplicated every side effect in those steps."

**More than one server.** Another [open issue](https://github.com/mastra-ai/mastra/issues/18877), labeled urgent, reports duplicate Slack replies when running more than one instance, because a deduplication cache is a per-process map. A [feature request](https://github.com/mastra-ai/mastra/issues/19252) still open since July notes that "everything live about a session is process-local," including its event bus and approval gate.

**State that outgrows memory.** Before a fix in August, a [production user found](https://github.com/mastra-ai/mastra/issues/17738) a single suspended run saved as a 170 MB compressed snapshot. Another user in the same thread measured snapshots growing quadratically when tool approval was on. Smaller limits remain by design: a durable run's stream can be replayed for only 30 seconds after it ends unless you change `cleanupTimeoutMs`.

## Upgrades and Licensing

Mastra moves quickly. `@mastra/core` reached 1.0 in January 2026 and has shipped [more than 80 minor versions](https://www.npmjs.com/package/@mastra/core?activeTab=versions) since. Its multi-agent API has been [replaced twice](https://mastra.ai/reference/migrations/network-to-supervisor), most recently after 1.0. The [1.0 upgrade](https://mastra.ai/reference/migrations/upgrade-to-v1/overview) needed a database migration for Postgres and LibSQL users.

Most of Mastra is Apache 2.0. Anything in an `ee/` folder is [source-available and needs a paid license](https://github.com/mastra-ai/mastra/blob/main/ee/LICENSE) in production, including built-in roles and permissions, Studio's single sign-on, and Agent Builder. The license lets Mastra move more features into that set later. Mastra has used three licenses since April 2025: Elastic 2.0, then Apache 2.0, then Apache 2.0 with the paid carve-out.

Your agent's code lives in your repo, but it's written against Mastra's `Agent` class, `createTool`, workflow syntax, memory, and hooks. Conversations live in Mastra's own database tables. Moving off Mastra means rewriting the agent and migrating its data.

## Omnara

[Omnara](https://github.com/omnara-ai/omnara) is an open-source managed agent platform under Apache 2.0. It runs the agent loop and everything around it as one service, so the support agent from earlier is a config:

```yaml
version: v1
instruction: You help customers with their orders.
model:
  provider_config: anthropic
  name: claude-sonnet-5.5
tools:
  get_order:
    type: custom
    description: Look up an order by its ID.
    input_schema:
      type: object
      properties:
        order_id: { type: string }
      required: [order_id]
```

Your backend runs `get_order` as an ordinary route, with the same types, database access, and tests as the rest of your app. Omnara sends each call to a webhook or your worker picks it up, and you post the result back through the API. Deploying or restarting your app never touches a running agent, because Omnara [holds each tool call](https://docs.omnara.com/tools/custom) until your backend answers.

Each failure above has an answer in Omnara's design. Every step an agent finishes is saved to Postgres as it happens, and workers are stateless. If a worker dies in a crash or a deploy, another continues the agent from its last saved step, with no recovery setting to turn on. A tool call that a crash cut off isn't rerun. It comes back to the agent [as interrupted, with its outcome unknown](https://docs.omnara.com/agents/failure-handling), so the agent can check before trying again.

Running more than one server needs nothing extra, because no agent's state lives in a worker's memory. [Approvals](https://docs.omnara.com/events/interactions) wait in Postgres as long as a person needs and hold no worker. Organization and project roles are enforced on every API call, with no separate license.

Omnara also has Slack, Discord, and GitHub integrations, cron triggers, memory stores, subagents, and skills built in. It works with any model from OpenAI, Anthropic, OpenRouter, Amazon Bedrock, or a compatible endpoint, on your own keys, and each subagent can use a different one.

Self-hosted, Omnara runs on [Postgres, Redis, blob storage, and a few services](https://docs.omnara.com/self-hosting/architecture), which is close to the stack a production Mastra deployment needs anyway. Self-hosting is free, and Omnara's team can also deploy and manage it in your own cloud (BYOC) or on premises. You can export all your data at any time, including every agent's full history, and run it on your own self-hosted Omnara by emailing contact@omnara.com.

On Omnara Cloud, there's nothing to run and no platform fee, and your own model keys and machines cost nothing. Models bought through Omnara cost the provider's token rates, and its machines are billed by usage. Both are paid with credits that carry a 5% service fee covering provider costs.

## Which One to Use

Mastra fits when the agent is a small piece of a TypeScript app you already run, and its tools should call your code in the same process. It also fits when you need code inside the loop, a workflow engine, or built-in evals and Studio's local debugging.

For an agent people depend on, choose Omnara. It configures the same parts of an agent that Mastra's hooks do, and it already runs the platform that teams end up building around Mastra. A crash never reruns a tool, a deploy never loses an agent's progress, and you can change a running agent without a redeploy.
