Crux
CookbookRuntime Recipes

Temporal Runtime Mapping

Recipe-only code sketch for mapping Crux flows and tasks onto Temporal workflows and activities.

Temporal can own much of durable execution, but it is recipe guidance in v1, not a bundled first-party Crux runtime adapter. A Temporal adapter should preserve Crux target names and diagnostics while mapping execution onto workflows, signals, timers, and activities.

Crux target declarations

Keep user code in Crux terms. A future adapter can discover these exported targets and register Temporal workers from the manifest.

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

export const chargeCustomer = durableTask('charge-customer', {
  run: async (input: { invoiceId: string }, _context: RuntimeTaskContext) => {
    const { invoiceId } = input
    await billing.charge(invoiceId)
  },
})

export const invoiceFlow = flow('invoice', async (flow, input: { invoiceId: string }) => {
  await flow.after(chargeCustomer, '1d', { invoiceId: input.invoiceId })

  return flow.waitFor(
    { name: 'invoice.paid' },
    { match: { invoiceId: input.invoiceId }, timeout: '7d' },
  )
})

Temporal workflow sketch

One mapping is one Temporal workflow per Crux flow occurrence, with Crux event waits represented as Temporal signals.

import { condition, defineSignal, proxyActivities, setHandler, sleep } from '@temporalio/workflow'

const invoicePaid = defineSignal<[payload: { invoiceId: string; paidAt: string }]>('invoice.paid')

const activities = proxyActivities<{
  runCruxTask(targetName: string, input: unknown): Promise<void>
}>({
  startToCloseTimeout: '5 minutes',
})

export async function invoiceWorkflow(input: { invoiceId: string }) {
  let paid: { invoiceId: string; paidAt: string } | undefined

  setHandler(invoicePaid, (payload) => {
    if (payload.invoiceId === input.invoiceId) paid = payload
  })

  await sleep('1 day')
  await activities.runCruxTask('charge-customer', { invoiceId: input.invoiceId })

  await condition(() => paid !== undefined, '7 days')
  if (!paid) throw new Error('invoice.paid timed out')

  return paid
}

Worker activity sketch

The activity resolves a Crux durable task target by durable target name. A real adapter should use generated artifacts rather than a hand-written map.

import { Worker } from '@temporalio/worker'
import { chargeCustomer } from './targets'

const targets = {
  'charge-customer': chargeCustomer,
}

await Worker.create({
  workflowsPath: require.resolve('./workflows'),
  taskQueue: 'crux-runtime',
  activities: {
    async runCruxTask(targetName: keyof typeof targets, input: unknown) {
      const target = targets[targetName]
      if (!target) {
        throw new Error(`TARGET_NOT_FOUND: ${targetName}`)
      }

      await target.execute({
        work: makeTemporalWorkItem(targetName, input),
        lease: makeTemporalLease(),
      })
    },
  },
})

Mapping notes

Runtime concernTemporal substrate
Flow runtimeTemporal workflow
Runtime task targetTemporal activity
SignalsTemporal signals or updates
TimersTemporal timers
Read-only inspectionTemporal queries plus Crux metadata

The adapter must define which Crux primitives are represented as workflows versus activities, and it must keep Crux diagnostics visible for replay drift, missing targets, and non-JSON payloads.

On this page