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/resume

Anti-patterns

  • Using Effect.sleep for durable workflow waits. Use sleep({...}) so timer events are recorded and the durable clock is used.
  • Forever-loop cron workflows. Use *.cron.tsx schedules 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.