Long-running work that
actually finishes.
The thing nobody wants to build: a job runner that survives deploys, retries the right step the right number of times, doesn't double-charge on a redrive, and lets you sleep a workflow for a week without holding a process open. Voltro ships it — built on @effect/workflow + @effect/cluster, the same primitives Effect-TS apps use in production today.
// onboarding.workflow.tsx — DESCRIPTOR (browser-safe)
import { workflow } from '@voltro/workflow/define'
import { Schema } from 'effect'
export const Onboarding = workflow({
name: 'user.onboarding',
payload: { userId: Schema.String },
success: Schema.Struct({ ok: Schema.Boolean }),
})
// onboarding.workflow.server.tsx — EXECUTOR (server-only)
import { step, sleep } from '@voltro/workflow'
import { Effect, Schema } from 'effect'
import { generateText } from '@voltro/ai'
import type { AppContext } from '@voltro/runtime'
export default (ctx: AppContext) => ({ userId }) =>
Effect.gen(function* () {
const user = yield* step({
name: 'load-user',
success: Schema.Struct({ email: Schema.String, firstName: Schema.String }),
execute: Effect.tryPromise(() =>
ctx.store.select('users').where('id', userId).one()),
})
yield* step({
name: 'send-welcome',
execute: Effect.tryPromise(() =>
ctx.store.insert('outbox', { to: user.email, template: 'welcome' })),
})
// Crash here? Resume replays from the journal —
// the welcome won't be re-sent. Durable, not Effect.sleep.
yield* sleep({ name: 'nudge-delay', duration: '1 day' })
const nudge = yield* generateText({
prompt: `Write a 1-sentence nudge for ${user.firstName}.`,
})
yield* step({
name: 'send-nudge',
execute: Effect.tryPromise(() =>
ctx.store.insert('outbox', { to: user.email, body: nudge.text })),
})
return { ok: true }
})Six things you don't have to write.
A workflow file declares its payload schema, success schema, and an executor. Everything below is provided — no helper code, no third-party SDK, no separate queue process.
Resumable steps
Each step()-wrapped call records its result. On crash, deploy, or replica failover the next run replays the journal — completed steps return their cached value, only the failing step re-executes.
Idempotency keys
Pass an idempotencyKey (payload → string) to a workflow. Concurrent starts with the same key collapse to one execution — no duplicate sends, no race-condition double-charges.
Sleep + delayed continuation
Effect.sleep("1 day") inside a workflow suspends durably. The runtime parks the execution, persists the wake-up time, and resumes on a different replica days later if your fleet rotated.
External signals
awaitSignal(ctx, name) parks the workflow until a human (or another service) POSTs a matching signal. The wait is durable; restarts don't lose pending approvals.
Cron + scheduled runs
*.cron.tsx files declare clock-triggered handlers with a mandatory IANA timezone. Multi-instance coordination is built-in: postgres advisoryLock by default, or cluster shard ownership for the same model the workflow engine uses.
Cluster-aware
Built on @effect/cluster. Run on one box (single coordinator) or a fleet of any size (shard-distributed). Workflows migrate between runners on death. SQL-backed state is the only requirement.
The four-system integration you don't have to wire.
Without a durable workflow primitive, every long-running task forces a four-way decision: pick a queue (Bull, Sidekiq, SQS), pick a scheduler (k8s CronJob, EventBridge), pick a coordination story (advisory lock, leader election), and pick a state store for resumption (Postgres, Redis, etc). Each layer has its own auth, observability gap, and on-call page. Workflow primitives collapse it to one declaration.
When to reach for a workflow:
- • Multi-step business logic that must survive a crash (onboarding, checkout fulfilment, refund flow).
- • External API calls you can't safely re-do on retry (Stripe charge, email send, AI call you pay for per token).
- • Anything that needs to wait (delayed continuation, scheduled follow-up, human approval).
- • Periodic work (nightly invoice generation, hourly metrics roll-up, weekly digest).
Workflows compose with the rest of the runtime.
AI primitives
Agent loops are a natural fit for workflows: the model call, the tool call, the human approval — all durable steps.
Type safety
Workflow payload + success types flow through the wire. Triggering from a mutation is type-checked end-to-end.
Reactive queries
A workflow step that updates a row triggers the reactive engine. Subscribers see progress live without polling.
Open the framework. See it for yourself.
Every primitive on this page is in the framework today. Clone the starter, run `voltro dev`, and have it on screen in two minutes.