# Pleach + Mastra (/docs/with-mastra)



This page is the *coexist* pattern: `@pleach/core` as the LLM-turn
substrate inside a Mastra workflow step, neither package
replacing the other. The other coexist patterns —
[Anthropic SDK](/docs/with-anthropic-sdk),
[OpenAI SDK](/docs/with-openai-sdk),
[Inngest](/docs/with-inngest) — follow the same posture; if
you're moving off a different orchestration stack, see the
[Migrate & coexist](/docs/migrating-from-langchain) docs.

Mastra and Pleach aren't competing for the same slot. Mastra
wraps the **workflow** — `Workflow` and `Agent` primitives,
step-based durable execution, vector DB integrations, an evals
module, a hosted observability dashboard. Pleach owns the **LLM
turn inside the workflow step** — the typed `AuditableCall` row
in *your* Postgres, family-locked provider routing,
replay-deterministic streaming, subagent cost rollup to the
parent `turnId`.

If you're already on Mastra, don't rip it out. Wrap your
`runtime.executeMessage` call inside a Mastra workflow step — or
use Pleach's `SessionRuntime` as the engine behind what would
otherwise be a Mastra `Agent`. The hosted dashboard, the workflow
graph, and the evals stay; the agent turn underneath gains the
audit row, the family lock, and the replay guarantee.

## The architecture [#the-architecture]

```
┌─────────────────────────────────────────────────────────────┐
│  Mastra Workflow                                            │
│  - Step-based durable execution                             │
│  - Hosted observability dashboard                           │
│  - Vector DB integration                                    │
│  - Evals module                                             │
│  - Agent network / suspend + resume                         │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  step.execute(async ({ inputData }) => {              │  │
│  │                                                       │  │
│  │    Pleach SessionRuntime                              │  │
│  │    - AuditableCall row → your Postgres                │  │
│  │    - Family-lock at session start                     │  │
│  │    - Replay-deterministic StreamEvent                 │  │
│  │    - Subagent rollup to parent turnId                 │  │
│  │                                                       │  │
│  │  })                                                   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

Mastra's workflow events and hosted dashboard are the
**operational** trail — what step ran, what suspended, what the
graph looked like, what the eval scored. Pleach's `AuditableCall`
ledger is the **business** trail — what tools the agent invoked,
which subagents spawned, what each cost, attributed to the turn
the user typed, written to a row in *your* Postgres. Finance
reads Pleach; product and on-call read Mastra.

## Vocabulary mapping [#vocabulary-mapping]

If you're coming from Mastra docs, the rough one-to-one:

| Mastra                                         | Pleach                                                          | Notes                                                                                                                                                                                                |
| ---------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Agent`                                        | `SessionRuntime` + a system prompt                              | Pleach has no `Agent` class — the system prompt is plugin-contributed; the loop is `runtime.executeMessage`.                                                                                         |
| `agent.generate({ messages })`                 | `for await (const e of runtime.executeMessage(sessionId, msg))` | Pleach is stream-first. Collect into a final string with `collectStream(...)` if you want generate-shaped sync.                                                                                      |
| `agent.stream(...).fullStream`                 | `runtime.executeMessage(...)` (the same async iterable)         | The Pleach `StreamEvent` discriminated union is richer than `fullStream` — tool / subagent / interrupt / checkpoint events.                                                                          |
| `model: "openai/gpt-4o"` (model-router string) | `new AiSdkProvider({ model: openrouter("openai/gpt-4o") })`     | Pleach goes through the AI SDK provider; OpenRouter gives the same `<family>/<model>` swap pattern. See [Providers → Same shape, different provider](/docs/providers#same-shape-different-provider). |
| `Workflow` + `createStep`                      | Not a Pleach primitive                                          | Workflows are out of scope for `@pleach/core`. Compose with Mastra (this page), Inngest ([with-inngest](/docs/with-inngest)), or LangGraph.                                                          |
| Scorers attached to an agent                   | `@pleach/eval` `new EvalSuite({ ... }).run()`                   | Pleach evals are CI-shaped, not in-line. Production sampling is `@pleach/observe`'s job.                                                                                                             |
| Memory primitives                              | `@pleach/core` Memory + `MemoryAdapter`                         | Similar conceptually; see [Memory](/docs/memory).                                                                                                                                                    |
| Hosted Mastra Studio (`localhost:4111`)        | No Pleach equivalent                                            | The explicit trade — see [Playground & devtools](/docs/playground-and-devtools).                                                                                                                     |

If you've never read Mastra docs, skip this table — it's only
useful for translating between two vocabularies.

## The code shape [#the-code-shape]

```typescript
// mastra/index.ts
import { Mastra } from "@mastra/core";
import { createWorkflow, createStep } from "@mastra/core/workflows";
import {
  createPleachRuntime,
  type StreamEvent,
} from "@pleach/core";
import { SupabaseAdapter } from "@pleach/core/sessions";
import { AiSdkProvider } from "@pleach/core";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

// Mastra's step primitive — durable, observable, replayable
// at the workflow layer. The body wraps a Pleach turn.
const agentTurnStep = createStep({
  id: "agent-turn",
  inputSchema: z.object({
    sessionId: z.string(),
    tenantId: z.string(),
    userId: z.string(),
    prompt: z.string(),
  }),
  outputSchema: z.object({
    events: z.array(z.any()),
  }),
  execute: async ({ inputData }) => {
    // Build the runtime per request. createPleachRuntime is the
    // zero-config factory — pass the per-request fields.
    const runtime = createPleachRuntime({
      tenantId: inputData.tenantId,
      userId: inputData.userId,
      storage: new SupabaseAdapter({ client: supabase }),
      provider: new AiSdkProvider({
        model: anthropic("claude-sonnet-4-5"),
      }),
    });

    // Inside the Mastra step, runtime.executeMessage gives you
    // the AuditableCall row in your Postgres, family-lock, replay
    // determinism, and the subagent rollup — none of which
    // Mastra's workflow events track at the row level.
    const collected: StreamEvent[] = [];
    for await (const evt of runtime.executeMessage(
      inputData.sessionId,
      inputData.prompt,
    )) {
      collected.push(evt);
    }
    return { events: collected };
  },
});

export const chatWorkflow = createWorkflow({
  id: "chat-turn",
  inputSchema: agentTurnStep.inputSchema,
  outputSchema: agentTurnStep.outputSchema,
})
  .then(agentTurnStep)
  .commit();

export const mastra = new Mastra({
  workflows: { chatWorkflow },
});
```

Three things this pattern gives you that a plain Mastra `Agent`
doesn't:

1. **The `AuditableCall` row lands in your Postgres** while the
   step is running. Finance can `SELECT SUM(token_usage) FROM
   harness_auditable_calls WHERE tenant_id = ? AND created_at > ?`
   without parsing Mastra's workflow event store, which is shaped
   for the dashboard. The row is keyed
   `(sessionId, turnId, stageId, seqWithinTurn)`, joinable to
   billing and to a compliance review, and it exists whether the
   Mastra step succeeds, suspends, or fails — it was written
   during the LLM call, not at workflow completion.
2. **Family-lock survives step retry and suspend/resume.** If the
   Mastra step resumes after an interrupt, Pleach replays against
   the same family + transport the session locked at start —
   never silently widens to a different provider mid-conversation.
   Mastra's workflow primitive is provider-agnostic by design;
   the lock lives at the Pleach layer.
3. **Subagents spawned by the agent roll their cost back to the
   parent `turnId`** via `SpawnTreeState`. Mastra's agent
   network lets you compose multiple agents and its workflows can
   fan out, but per-token cost attribution between a parent turn
   and its child spawns is your code's problem. Pleach's spawn
   record keys it for you.

## Using Pleach instead of `Agent` inside a Mastra workflow [#using-pleach-instead-of-agent-inside-a-mastra-workflow]

The pattern above wraps a Pleach turn inside a Mastra step. An
adjacent pattern is to keep Mastra's workflow + dashboard + evals
and use Pleach's `SessionRuntime` as the underlying engine for
what would otherwise be a Mastra `Agent`. The workflow node calls
`runtime.executeMessage` rather than `agent.generate` /
`agent.stream`. You keep Mastra's orchestration surface and you
get the Pleach audit row + family-lock + replay determinism in
exchange for hand-wiring the agent-shape Mastra's `Agent` class
gives you for free.

Both shapes are honest. Pick the wrap when the team is already
shipping Mastra `Agent`s and you want the audit row added. Pick
the replace when you're starting fresh and want the runtime
substrate to be the agent.

## Where each is load-bearing [#where-each-is-load-bearing]

| Concern                                                | Mastra's slot                             | Pleach's slot                                                  |
| ------------------------------------------------------ | ----------------------------------------- | -------------------------------------------------------------- |
| Workflow primitive (`Workflow`, steps, suspend/resume) | Built-in                                  | None — Pleach is per-turn                                      |
| Hosted observability dashboard                         | Built-in (their key DX advantage)         | None — bring Datadog / Honeycomb                               |
| Vector DB integration                                  | First-class (multiple stores)             | None — DIY behind a tool                                       |
| Evals module                                           | Built-in (`@mastra/evals`)                | Strict-mode replay against the ledger (planned `@pleach/eval`) |
| Agent abstraction (`Agent` class)                      | Built-in (instructions + tools + model)   | `SessionRuntime` + plugin contract                             |
| Per-call audit row in YOUR schema                      | Workflow events are dashboard-shaped      | `AuditableCall` row keyed `(tenantId, turnId)`                 |
| Tamper-evident audit trail                             | None at the row level                     | `prev_hash` + `row_hash` in your DB                            |
| Family-locked provider routing                         | None — provider-agnostic by design        | locked at `runtime.sessions.create({ provider, model })`       |
| Deterministic replay of the LLM stream                 | Workflow-level replay (rerun steps)       | Fingerprint replays byte-identical `StreamEvent`s              |
| Subagent cost rollup to parent `turnId`                | Agent network composes, doesn't roll cost | `SpawnTreeState`                                               |
| Time-travel checkpoints inside a session               | Workflow suspend/resume                   | `runtime.checkpoints.rollback()` / `.list()`                   |
| Multi-tenant `tenant_id` stamping on every row + span  | Consumer responsibility                   | `runtime.tenant` facet + CI gates                              |
| Hosted control plane (cloud)                           | Yes (Mastra Cloud)                        | None — Pleach is a runtime library                             |

The coverage maps overlap on "agent abstraction" and "evals" —
both ship those, with different shapes. They diverge sharply
everywhere else: Mastra owns the workflow graph and the hosted
dashboard; Pleach owns the row in your DB and the replay
guarantee. That's why the partnership reads cleanly — neither
package is doing the other's job badly.

## When you don't need Mastra [#when-you-dont-need-mastra]

* A per-request agent turn that completes inside one HTTP
  request, with no scheduled jobs, no fan-out, no vector DB
  needs, and no workflow graph to model. You can call
  `runtime.executeMessage` directly from an API route and skip
  the workflow layer entirely.
* The Pleach checkpointer covers your in-session time-travel.
  Cross-step workflow durability — suspend a step, resume it
  later, fan out across N branches, schedule a cron — is
  Mastra's slot, not Pleach's.

## When you don't need Pleach [#when-you-dont-need-pleach]

* Single-tenant prototype with no audit obligation, no per-tenant
  cost rollup, and no replay requirement. Mastra alone is enough
  — the hosted dashboard gives you a perfectly good operational
  view for that shape.
* The [comparison page](/docs/comparison) covers when the AI SDK
  alone is enough and when Mastra alone is enough. Reach for
  Pleach when the audit row, the family lock, or the replay
  guarantee shows up on the requirements list.

## Mastra Cloud, the dashboard, and the bits Pleach intentionally doesn't ship [#mastra-cloud-the-dashboard-and-the-bits-pleach-intentionally-doesnt-ship]

Mastra's hosted dashboard is the part of the product Pleach has
no equivalent for, and intentionally so. Pleach is a library that
writes rows into a database you own. The dashboard you build on
top of those rows is yours to ship — wire Datadog, Honeycomb,
Grafana, or a custom Postgres view; the
[OTel spans](/docs/otel-observability) and the
[`AuditableCall` row](/docs/auditable-call-row) are the materials.

If you want a hosted dashboard out of the box, Mastra ships one.
If you want the rows in your own schema with no vendor lock-in on
the audit surface, Pleach ships that. The pairing pattern gives
you both: Mastra's dashboard for the workflow trail, your own
Postgres for the audit trail.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Pleach + Inngest" href="/docs/with-inngest" description="The same coexist posture for durable-execution platforms — Inngest, Trigger.dev, DBOS, Temporal." />

  <Card title="Pleach + Anthropic SDK" href="/docs/with-anthropic-sdk" description="Coexist with the Anthropic SDK as transport — prompt caching, the latest tools API, batches, files." />

  <Card title="Pleach + OpenAI SDK" href="/docs/with-openai-sdk" description="Coexist with the OpenAI SDK — Chat Completions, the Responses API, structured outputs." />

  <Card title="The AuditableCall row" href="/docs/auditable-call-row" description="What lands in your Postgres on every LLM call, and what you can join it against." />
</Cards>
