Flow control
Durable queues, bounded concurrency, and rate limits inside workflow code.
Use flow control when the workflow is durable, but the resource it touches is limited: a third-party API, a tenant import lane, a GPU job, or a webhook fan-out.
Voltro exposes the workflow engine's durable queue and rate limiter through @voltro/workflow:
import {
durableQueue,
processQueue,
queueWorker,
rateLimit,
step,
workflow,
} from '@voltro/workflow'
import { Effect, Schema } from 'effect'Bounded concurrency
Define a queue once, then process items from workflow code. A worker layer controls concurrency and is backed by the same SQL/cluster workflow engine, so another runner can resume after a crash.
export const ThumbnailQueue = durableQueue({
name: 'thumbnails',
payload: {
imageId: Schema.String,
sourceUrl: Schema.String,
},
success: Schema.Struct({ thumbnailUrl: Schema.String }),
idempotencyKey: ({ imageId }) => imageId,
})
export const thumbnailWorker = queueWorker(
ThumbnailQueue,
({ imageId, sourceUrl }) =>
step({
name: 'render-thumbnail',
input: { imageId },
success: Schema.Struct({ thumbnailUrl: Schema.String }),
execute: renderThumbnail(sourceUrl),
}),
{ concurrency: 4 },
)Export the worker from any discovered *.workflow.tsx module. voltro dev and voltro serve auto-mount branded queueWorker(...) layers at boot; no app.config.ts layer plumbing is needed.
Inside a workflow:
const result = yield* processQueue(ThumbnailQueue, {
imageId,
sourceUrl,
})processQueue(...) is a workflow activity. The item is persisted, the workflow parks while a worker processes it, and replay returns the recorded worker result instead of re-enqueueing.
Rate limits
Use rateLimit(...) before a step that talks to a constrained service:
yield* rateLimit({
name: 'stripe-write',
window: '1 minute',
limit: 100,
key: tenantId,
})
const invoice = yield* step({
name: 'create-invoice',
input: { tenantId, orderId },
execute: createStripeInvoice(orderId),
})The wait uses the durable workflow clock. A deploy or worker crash does not lose the delay.
Dashboard behavior
Queue workers and rate-limited steps still record normal workflow steps, timers, and run events. The Workflows dashboard shows the parked run, the step that is waiting, and the later continuation through the same live run/step/event subscriptions. The Flow tab groups visible runs by workflow lane, queued/running/waiting status, and start source so pressure is visible in both local devtools and Voltro Cloud.
Rules of thumb
- Use a durable queue for bounded concurrency and per-resource lanes.
- Use
rateLimit(...)for API budgets where excess work should wait, not fail. - Put the limiting key in your payload or workflow state so replay is deterministic.
- Keep external I/O inside
step(...)or the queue worker body.