pleach
Integrate

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

MastraPleachNotes
AgentSessionRuntime + a system promptPleach 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(...).fullStreamruntime.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 + createStepNot a Pleach primitiveWorkflows 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 + MemoryAdapterSimilar conceptually; see Memory.
Hosted Mastra Studio (localhost:4111)No Pleach equivalentThe 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:

  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

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

ConcernMastra's slotPleach's slot
Workflow primitive (Workflow, steps, suspend/resume)Built-inNone — Pleach is per-turn
Hosted observability dashboardBuilt-in (their key DX advantage)None — bring Datadog / Honeycomb
Vector DB integrationFirst-class (multiple stores)None — DIY behind a tool
Evals moduleBuilt-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 schemaWorkflow events are dashboard-shapedAuditableCall row keyed (tenantId, turnId)
Tamper-evident audit trailNone at the row levelprev_hash + row_hash in your DB
Family-locked provider routingNone — provider-agnostic by designlocked at runtime.sessions.create({ provider, model })
Deterministic replay of the LLM streamWorkflow-level replay (rerun steps)Fingerprint replays byte-identical StreamEvents
Subagent cost rollup to parent turnIdAgent network composes, doesn't roll costSpawnTreeState
Time-travel checkpoints inside a sessionWorkflow suspend/resumeruntime.checkpoints.rollback() / .list()
Multi-tenant tenant_id stamping on every row + spanConsumer responsibilityruntime.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.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

  • 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

On this page