Crux
CookbookRuntime Recipes

Inngest Runtime Mapping

Recipe-only code sketch for mapping Crux runtime targets onto Inngest functions and events.

Inngest is a recipe and adapter-author target in v1, not a bundled Crux runtime package. The goal is to keep Crux authoring stable while an adapter maps wake delivery and retries onto Inngest infrastructure.

User-authored Crux targets

Users should still write Crux flows and durable tasks, not Inngest-specific functions.

import { flow } from '@use-crux/core/flow'
import { durableTask, type RuntimeTaskContext } from '@use-crux/core/runtime'

export const embedDocument = durableTask('embed-document', {
  run: async (input: { documentId: string }, _context: RuntimeTaskContext) => {
    const { documentId } = input
    await embeddings.write(documentId)
  },
})

export const reviewFlow = flow('review', async (flow, input: { documentId: string }) => {
  await flow.defer(embedDocument, { documentId: input.documentId })

  const approval = await flow.waitFor(
    { name: 'document.approved' },
    { match: { documentId: input.documentId }, timeout: '24h' },
  )

  return approval
})

Wake bridge sketch

An Inngest-backed wake adapter can publish one event per Crux wake. Use the Crux idempotency key as the event id when the substrate supports it.

import { Inngest } from 'inngest'
import type { RuntimeWakeAdapter } from '@use-crux/core/runtime'

const inngest = new Inngest({ id: 'app' })

export function inngestWake(): RuntimeWakeAdapter {
  return {
    id: 'inngest',
    capabilities: {
      signed: true,
      maxPayloadBytes: 4 * 1024,
      maxDelayMs: 7 * 24 * 60 * 60 * 1000,
    },
    createWake({ url }) {
      return async (envelope) => {
        await inngest.send({
          name: 'crux/runtime.wake',
          id: envelope.idempotencyKey,
          data: {
            url,
            envelope,
          },
        })
      }
    },
  }
}

Inngest function sketch

The Inngest function acts as a delivery worker. It forwards the wake into the same fetch-compatible Crux handler used by generated serverless entries.

import { createRuntimeHandler } from '@use-crux/core/runtime'
import { embedDocument, reviewFlow } from './targets'

const handler = createRuntimeHandler({
  targets: [reviewFlow, embedDocument],
})

export const cruxWake = inngest.createFunction(
  { id: 'crux-runtime-wake' },
  { event: 'crux/runtime.wake' },
  async ({ event, step }) => {
    const body = JSON.stringify(event.data.envelope)

    const response = await step.run('deliver-to-crux', () =>
      handler.POST(
        new Request(event.data.url, {
          method: 'POST',
          body,
          headers: {
            'content-type': 'application/json',
            // A real adapter should sign this request or bind directly without HTTP.
            'x-crux-signature': signCruxWake(body),
          },
        }),
      ),
    )

    if (response.status >= 500 || response.status === 409) {
      throw new Error(`Crux wake retryable status ${response.status}`)
    }
  },
)

Mapping notes

Runtime concernInngest substrate
Wake/tasks/timers/retriesInngest functions, events, and steps
Event waitsInngest wait-for-event style waits or Crux waiter state
State/events/leasesCrux store or adapter-owned Inngest-backed state

Do not split one invariant silently between Crux and Inngest. If Inngest owns retries or timers, the adapter must still surface Crux diagnostics such as TARGET_NOT_FOUND, REPLAY_DIVERGED, and WORK_DEAD_LETTERED.

On this page