Sleep & wakeups
Durable sleep, cron schedules, external signals, and how parked workflows show up in inspection.
Use sleep({...}) from @voltro/workflow for durable waits inside a workflow. It wraps @effect/workflow's DurableClock.sleep and records timer events for the dashboard.
Basic sleep
import { workflow, step, sleep } from '@voltro/workflow'
import { Effect, Schema } from 'effect'
const buildExecute = (ctx: AppContext) =>
({ userId }: { userId: string }) =>
Effect.gen(function* () {
yield* step({
name: 'send-welcome',
success: Schema.Void,
execute: sendEmail(ctx, userId, 'welcome'),
})
yield* sleep({ name: 'retention-delay', duration: '7 days' })
yield* step({
name: 'send-retention-check-in',
success: Schema.Void,
execute: sendEmail(ctx, userId, 'retention-check-in'),
})
})The workflow is not meant to hold a worker thread for the whole delay. The durable clock parks the workflow in the engine; Voltro records timer-set and timer-fired events in _voltro_workflow_run_events.
Sleep until a wall-clock time
Compute a duration, then pass it to sleep:
const millisUntilNextThreeUtc = () => {
const now = new Date()
const target = new Date(now)
target.setUTCHours(3, 0, 0, 0)
if (target <= now) target.setUTCDate(target.getUTCDate() + 1)
return target.getTime() - now.getTime()
}
yield* sleep({
name: 'wait-until-3am-utc',
duration: `${millisUntilNextThreeUtc()} millis`,
})Use this for one-off waits that are part of a larger workflow. For recurring cadence, use schedules.
Recurring work belongs in schedules
Do not build a forever loop with sleep for "run every day" jobs. Use *.cron.tsx:
// apps/api/schedules/daily-rollup.cron.tsx
import { defineSchedule } from '@voltro/runtime'
export default defineSchedule({
name: 'rollup.daily',
cron: '0 3 * * *',
timezone: 'UTC',
onOverlap: 'skip',
handler: async ({ app, scheduledAt }) => {
await runRollup(app.store, scheduledAt)
},
})Schedules give you timezone parsing, overlap policy, backfill policy, cluster-wide coordination, run history, and manual "Run now" controls.
External wakeups with awaitSignal
Use awaitSignal(ctx, ...) when a workflow must wait for an outside decision, such as a human approval or webhook callback.
import { awaitSignal, step } from '@voltro/workflow'
import { Effect, Schema } from 'effect'
const decision = yield* awaitSignal(ctx, {
name: 'approval',
schema: Schema.Struct({
approved: Schema.Boolean,
reviewerId: Schema.String,
}),
timeoutMs: 24 * 60 * 60_000,
})
if (!decision.approved) {
yield* step({
name: 'cancel-order',
success: Schema.Void,
execute: cancelOrder(orderId, decision.reviewerId),
})
}Signals are matched by name inside one workflow run. The payload is decoded with the supplied schema.
Sending signals
From the dashboard, open a running workflow and use "Send signal".
From the CLI:
voltro workflows signal wfrun_01H... --name approval --payload '{"approved":true,"reviewerId":"usr_123"}'From HTTP:
POST /_voltro/inspect/workflows/runs/:id/signal
Content-Type: application/json
{ "signalName": "approval", "payload": { "approved": true, "reviewerId": "usr_123" } }The runtime records signal-awaited, signal-sent, and signal-received events in _voltro_workflow_run_events. The polling activity inside awaitSignal is checkpointed; once it receives a payload, replay returns the journaled value.
Cancellation and suspend/resume
Use operational controls, not sentinel rows:
voltro workflows cancel wfrun_01H...
voltro workflows suspend wfrun_01H...
voltro workflows resume wfrun_01H...The inspect API exposes the same actions at:
POST /_voltro/inspect/workflows/runs/:id/cancel
POST /_voltro/inspect/workflows/runs/:id/suspend
POST /_voltro/inspect/workflows/runs/:id/resumeAnti-patterns
- Using
Effect.sleepfor durable workflow waits. Usesleep({...})so timer events are recorded and the durable clock is used. - Forever-loop cron workflows. Use
*.cron.tsxschedules for recurring cadence. - Polling an external system with long sleeps. Prefer webhooks or
awaitSignal. - Sleeping inside a database transaction. Finish the write boundary before the wait.