# Pleach + Inngest (/docs/with-inngest)



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](/docs/with-anthropic-sdk),
[OpenAI SDK](/docs/with-openai-sdk),
[Mastra](/docs/with-mastra) — follow the same posture; if
you're moving off a different stack entirely, see the
[Migrate & coexist](/docs/migrating-from-ai-sdk) 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 [#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 [#the-code-shape]

```typescript
// 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 [#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 `StreamEvent`s        |
| 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 [#when-you-dont-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 [#when-you-dont-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](/docs/comparison) covers when to stay on the
  AI SDK alone.

## Trigger.dev, DBOS, Temporal, durable-execution generally [#triggerdev-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 [#where-to-go-next]

<Cards>
  <Card title="Pleach + Mastra" href="/docs/with-mastra" description="The same coexist posture for workflows + the hosted observability dashboard." />

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