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 concern | Temporal substrate |
|---|---|
| Flow runtime | Temporal workflow |
| Runtime task target | Temporal activity |
| Signals | Temporal signals or updates |
| Timers | Temporal timers |
| Read-only inspection | Temporal 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.
Related
- Guide: Runtime Adapter Authoring
- Reference: Runtime Engine