pleach
Integrate

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:

  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 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.
  2. Family-lock survives step.run retry. 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.
  3. Subagents spawned by the agent roll their cost back to the parent turnId via SpawnTreeState. Inngest's step.invoke can spawn child functions but per-token cost attribution between parent and child is your code's problem.

Where each is load-bearing

ConcernInngest's slotPleach's slot
Crash recovery across function boundariesStep memoizationNone — Pleach is per-turn
Retry on transient errorsretries: config + backoffRe-runs inside the step
Cron / event-triggered executioninngest.createFunction triggersNone
Fan-out / map across N itemsstep.invoke parallelNone
Per-tenant LLM cost rollupManual via event.data + dashboard queryAuditableCall row keyed (tenantId, turnId)
Tamper-evident audit trailInngest run history (vendor-shaped)prev_hash + row_hash in your DB
Family-locked provider routingNonelocked at runtime.sessions.create({ provider, model })
Deterministic replay of the LLM streamStep memoization re-runs your codeFingerprint replays byte-identical StreamEvents
Subagent cost rollup to parentManualSpawnTreeState
Time-travel checkpoints inside a sessionNoneruntime.checkpoints.rollback() / .list()
Operational dashboard (what fired when)Built-inNone — 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.executeMessage directly 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

On this page