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.
This page is the coexist pattern: @pleach/core as the LLM-turn
substrate inside an Inngest step.run, neither package
replacing the other. The other coexist patterns —
Anthropic SDK,
OpenAI SDK,
Mastra — follow the same posture; if
you're moving off a different stack entirely, see the
Migrate & coexist docs.
Inngest and Pleach aren't competing for the same slot. Inngest
wraps the request — durable steps, retries, scheduling, cron,
fan-out, a hosted run dashboard. Pleach owns the LLM turn
inside the request — the typed AuditableCall row, family-locked
provider routing, replay-deterministic streaming, subagent cost
rollup to the parent turnId.
If you're already on Inngest, don't rip it out. Wrap your
executeMessage call in step.run and add Pleach as the substrate
inside the step. The same pattern applies to Trigger.dev,
DBOS, Temporal, and any other durable-execution platform.
The architecture
┌─────────────────────────────────────────────────────────────┐
│ Inngest function │
│ - Durable retry across crashes │
│ - Step memoization │
│ - Run dashboard │
│ - Cron / event-triggered │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ step.run("agent-turn", async () => { │ │
│ │ │ │
│ │ Pleach SessionRuntime │ │
│ │ - AuditableCall row → your Postgres │ │
│ │ - Family-lock at session start │ │
│ │ - Replay-deterministic StreamEvent │ │
│ │ - Subagent rollup to parent turnId │ │
│ │ │ │
│ │ }) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Inngest's run history is the operational trail — what fired,
what retried, what crashed. 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.
Finance reads Pleach; on-call reads Inngest.
The code shape
// inngest/handleChatTurn.ts
import { inngest } from "./inngest-client";
import {
createPleachRuntime,
type StreamEvent,
} from "@pleach/core";
import { SupabaseAdapter } from "@pleach/core/sessions";
import { AiSdkProvider } from "@pleach/core";
import { anthropic } from "@ai-sdk/anthropic";
export const handleChatTurn = inngest.createFunction(
{ id: "chat-turn", retries: 3 },
{ event: "chat/turn.requested" },
async ({ event, step }) => {
// Build the runtime once per request. createPleachRuntime is
// the zero-config factory — pass the per-request fields.
const runtime = createPleachRuntime({
tenantId: event.data.tenantId,
userId: event.data.userId,
storage: new SupabaseAdapter({ client: supabase }),
provider: new AiSdkProvider({
model: anthropic("claude-sonnet-4-5"),
}),
});
// step.run gives durable retry + memoization across crashes.
// Inside it, runtime.executeMessage gives you the AuditableCall
// row in your Postgres, family-lock, replay determinism, and
// the subagent rollup — none of which Inngest tracks.
const events = await step.run("agent-turn", async () => {
const collected: StreamEvent[] = [];
for await (const evt of runtime.executeMessage(
event.data.sessionId,
event.data.prompt,
)) {
collected.push(evt);
}
return collected;
});
return { events };
},
);Three things this pattern gives you that AgentKit alone 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 Inngest's run history. The row exists whether the Inngest step succeeds or retries or fails permanently — it was written during the LLM call, not at function return. - Family-lock survives
step.runretry. If the step retries because of a network blip, Pleach replays against the same family + transport the session locked at start — never silently widens to a different provider mid-conversation. - Subagents spawned by the agent roll their cost back to the
parent
turnIdviaSpawnTreeState. Inngest'sstep.invokecan spawn child functions but per-token cost attribution between parent and child is your code's problem.
Where each is load-bearing
| Concern | Inngest's slot | Pleach's slot |
|---|---|---|
| Crash recovery across function boundaries | Step memoization | None — Pleach is per-turn |
| Retry on transient errors | retries: config + backoff | Re-runs inside the step |
| Cron / event-triggered execution | inngest.createFunction triggers | None |
| Fan-out / map across N items | step.invoke parallel | None |
| Per-tenant LLM cost rollup | Manual via event.data + dashboard query | AuditableCall row keyed (tenantId, turnId) |
| Tamper-evident audit trail | Inngest run history (vendor-shaped) | prev_hash + row_hash in your DB |
| Family-locked provider routing | None | locked at runtime.sessions.create({ provider, model }) |
| Deterministic replay of the LLM stream | Step memoization re-runs your code | Fingerprint replays byte-identical StreamEvents |
| Subagent cost rollup to parent | Manual | SpawnTreeState |
| Time-travel checkpoints inside a session | None | runtime.checkpoints.rollback() / .list() |
| Operational dashboard (what fired when) | Built-in | None — bring Datadog / Honeycomb |
The two coverage maps barely overlap. That's why the partnership is honest — neither package is doing the other's job badly.
When you don't need Inngest
- A request-scoped agent turn that completes inside one HTTP
request, with no external waits, no cron, no fan-out. You can
call
runtime.executeMessagedirectly from an API route. - The Pleach checkpointer covers your in-session time-travel. Cross-request durability — i.e. "if the server crashes mid-turn, resume the turn on restart" — is Inngest's slot, not Pleach's.
When you don't need Pleach
- No multi-tenant concern, no audit obligation, no replay requirement. The AI SDK + Inngest combo is enough.
- Single-shot chatbot with tools, no persistence, no compliance surface. Pleach's overhead doesn't pay back at this size — the comparison page covers when to stay on the AI SDK alone.
Trigger.dev, DBOS, Temporal, durable-execution generally
The same pattern works for any durable-execution platform. The
shape is "wrap runtime.executeMessage inside the platform's
durable-task primitive." Pleach doesn't compete with any of them
for the durability slot — it occupies the LLM-turn slot inside the
durable task.
Where to go next
Pleach + Mastra
The same coexist posture for workflows + the hosted observability dashboard.
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 + 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.
MCP integration
Integrate MCP servers from `@pleach/core` today — the `defineTool` wrapper pattern. Distinct from the `@pleach/mcp` SKU (see [/docs/mcp]).