Pleach + Mastra
Coexist with Mastra — keep the workflow primitive, the hosted dashboard, vector DB, and evals; add `@pleach/core` for the audited LLM turn inside the step. Not a migration.
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,
OpenAI SDK,
Inngest — follow the same posture; if
you're moving off a different orchestration stack, see the
Migrate & coexist 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
┌─────────────────────────────────────────────────────────────┐
│ 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
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. |
Workflow + createStep | Not a Pleach primitive | Workflows are out of scope for @pleach/core. Compose with Mastra (this page), Inngest (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. |
Hosted Mastra Studio (localhost:4111) | No Pleach equivalent | The explicit trade — see Playground & devtools. |
If you've never read Mastra docs, skip this table — it's only useful for translating between two vocabularies.
The code shape
// 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:
- The
AuditableCallrow lands in your Postgres while the step is running. Finance canSELECT 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. - 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.
- Subagents spawned by the agent roll their cost back to the
parent
turnIdviaSpawnTreeState. 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
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 Agents 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
| 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 StreamEvents |
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
- 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.executeMessagedirectly 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
- 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 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'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 and the
AuditableCall 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
Pleach + Inngest
The same coexist posture for durable-execution platforms — Inngest, Trigger.dev, DBOS, Temporal.
Pleach + Anthropic SDK
Coexist with the Anthropic SDK as transport — prompt caching, the latest tools API, batches, files.
Pleach + OpenAI SDK
Coexist with the OpenAI SDK — Chat Completions, the Responses API, structured outputs.
The AuditableCall row
What lands in your Postgres on every LLM call, and what you can join it against.
Pleach + OpenAI SDK
Coexist with the OpenAI SDK. Keep Chat Completions, Responses API, structured outputs; add @pleach/core for AuditableCall, family-lock, replay.
Pleach + Inngest
Coexist with Inngest (or Trigger.dev / DBOS / Temporal) — keep durable steps, retries, and scheduling; add `@pleach/core` as the LLM-turn substrate inside the step. Not a migration.