Langlaufende Prozesse, die
wirklich fertig werden.

Das, was niemand bauen will: ein Job-Runner, der Deployments übersteht, den richtigen Schritt genau so oft wiederholt wie nötig, bei einem Redrive nicht doppelt abrechnet und einen Workflow eine Woche schlafen lässt, ohne einen Prozess offen zu halten. Voltro liefert das mit — gebaut auf @effect/workflow + @effect/cluster, denselben Primitiven, die Effect-TS-Apps heute in Produktion nutzen.

onboarding.workflow.tsx
TypeScript
// 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 }
  })

Sechs Dinge, die du nicht schreiben musst.

Eine Workflow-Datei deklariert ihr Payload-Schema, ihr Success-Schema und einen Executor. Alles Weitere unten ist bereits vorhanden — kein Hilfscode, kein Third-Party-SDK, kein separater Queue-Prozess.

Fortsetzbare Steps

Jeder in step() gewrappte Aufruf zeichnet sein Ergebnis auf. Bei Crash, Deploy oder Replica-Failover spielt der nächste Lauf das Journal erneut ab — abgeschlossene Steps liefern ihren gecachten Wert, nur der fehlgeschlagene Step wird erneut ausgeführt.

Idempotenz-Keys

Übergib einem Workflow einen idempotencyKey (payload → string). Gleichzeitige Starts mit demselben Key kollabieren zu einer einzigen Ausführung — keine doppelten Sends, keine Race-Condition-Doppelbuchungen.

Sleep + verzögerte Fortsetzung

Effect.sleep("1 day") in einem Workflow suspendiert dauerhaft. Die Runtime parkt die Ausführung, persistiert den Weckzeitpunkt und nimmt sie Tage später auf einer anderen Replica wieder auf, falls deine Flotte rotiert ist.

Externe Signale

awaitSignal(ctx, name) parkt den Workflow, bis ein Mensch (oder ein anderer Service) ein passendes Signal POSTet. Das Warten ist dauerhaft; Neustarts verlieren keine ausstehenden Freigaben.

Cron + geplante Läufe

*.cron.tsx-Dateien deklarieren uhrzeitgesteuerte Handler mit verpflichtender IANA-Zeitzone. Multi-Instance-Koordination ist eingebaut: standardmäßig Postgres advisoryLock oder Cluster-Shard-Ownership nach demselben Modell, das die Workflow-Engine nutzt.

Cluster-fähig

Gebaut auf @effect/cluster. Läuft auf einer einzelnen Maschine (Single Coordinator) oder einer Flotte beliebiger Größe (Shard-verteilt). Workflows migrieren beim Ausfall zwischen Runnern. Einzige Voraussetzung ist SQL-gestützter State.

Die Vier-System-Integration, die du nicht verdrahten musst.

Ohne ein Durable-Workflow-Primitiv erzwingt jede langlaufende Aufgabe eine Vier-Wege-Entscheidung: eine Queue wählen (Bull, Sidekiq, SQS), einen Scheduler wählen (k8s CronJob, EventBridge), eine Koordinationsstrategie wählen (Advisory Lock, Leader Election) und einen State-Store für die Wiederaufnahme wählen (Postgres, Redis, etc). Jede Schicht hat ihre eigene Auth, ihre eigene Observability-Lücke und ihre eigene On-Call-Seite. Workflow-Primitive kollabieren das zu einer einzigen Deklaration.

Wann man zu einem Workflow greift:

  • Mehrstufige Geschäftslogik, die einen Crash überstehen muss (Onboarding, Checkout-Fulfilment, Refund-Flow).
  • Externe API-Aufrufe, die man bei einem Retry nicht gefahrlos wiederholen kann (Stripe-Charge, E-Mail-Versand, AI-Aufruf, den du pro Token bezahlst).
  • Alles, was warten muss (verzögerte Fortsetzung, geplantes Follow-up, menschliche Freigabe).
  • Periodische Arbeit (nächtliche Rechnungserstellung, stündliches Metrics-Roll-up, wöchentlicher Digest).

Workflows komponieren mit dem Rest der Runtime.

Öffne das Framework. Schau es dir selbst an.

Jede Primitive auf dieser Seite ist heute im Framework. Klone den Starter, lass `voltro dev` laufen, in zwei Minuten ist es auf dem Bildschirm.