Temporal vs Restate vs Windmill: Durable Workflow Orchestration (2026)
TL;DR
Temporal is the durable execution platform — workflows as code, automatic retries, timers, signals, queries, the most battle-tested workflow orchestration. Restate is the durable execution engine — lightweight, SDK-native, virtual objects, journaled execution, no separate server for simple setups. Windmill is the developer-first workflow platform — scripts as workflows, UI builder, schedules, approval flows, self-hosted, TypeScript/Python/Go. In 2026: Temporal for mission-critical durable workflows, Restate for lightweight durable execution, Windmill for script-based automation.
Key Takeaways
- Temporal: @temporalio/client ~25K weekly downloads — durable execution, battle-tested
- Restate: @restatedev/restate-sdk ~3K weekly downloads — lightweight, journaled
- Windmill: 12K+ GitHub stars — scripts, UI builder, schedules, approval flows
- Temporal provides the most robust durable execution for complex workflows
- Restate offers the lightest-weight approach to durable functions
- Windmill combines workflow orchestration with a script management UI
Temporal
Temporal — durable execution platform:
Setup
npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity
# Start Temporal server (development):
temporal server start-dev
Workflow definition
// src/workflows.ts — deterministic workflow code
import { proxyActivities, sleep, condition, setHandler, defineSignal, defineQuery } from "@temporalio/workflow"
import type * as activities from "./activities"
const { fetchNpmData, updateDatabase, sendNotification, publishToRegistry } =
proxyActivities<typeof activities>({
startToCloseTimeout: "30s",
retry: {
maximumAttempts: 3,
initialInterval: "1s",
backoffCoefficient: 2,
},
})
// Signal and query definitions:
export const approvalSignal = defineSignal<[boolean]>("approval")
export const statusQuery = defineQuery<string>("status")
// Package publish workflow:
export async function publishPackageWorkflow(
packageName: string,
version: string,
publisherId: string
): Promise<{ success: boolean; publishedAt?: string }> {
let status = "validating"
let approved: boolean | undefined
// Set up signal handler for approval:
setHandler(approvalSignal, (isApproved: boolean) => {
approved = isApproved
})
// Set up query handler:
setHandler(statusQuery, () => status)
// Step 1: Fetch and validate npm data
status = "fetching-data"
const npmData = await fetchNpmData(packageName)
if (!npmData) {
status = "failed-validation"
return { success: false }
}
// Step 2: Update database
status = "updating-database"
await updateDatabase(packageName, version, npmData)
// Step 3: Wait for approval (up to 24 hours)
status = "awaiting-approval"
await sendNotification(
publisherId,
`Package ${packageName}@${version} ready for approval`
)
// Wait for signal or timeout:
const gotApproval = await condition(() => approved !== undefined, "24h")
if (!gotApproval || !approved) {
status = "rejected"
await sendNotification(publisherId, `Package ${packageName}@${version} was ${gotApproval ? "rejected" : "timed out"}`)
return { success: false }
}
// Step 4: Publish
status = "publishing"
await publishToRegistry(packageName, version)
// Step 5: Notify
status = "completed"
await sendNotification(
publisherId,
`Package ${packageName}@${version} published successfully!`
)
return { success: true, publishedAt: new Date().toISOString() }
}
// Scheduled data sync workflow:
export async function packageSyncWorkflow(packages: string[]): Promise<void> {
for (const pkg of packages) {
const data = await fetchNpmData(pkg)
if (data) {
await updateDatabase(pkg, data.version, data)
}
// Sleep between requests to avoid rate limiting:
await sleep("2s")
}
}
Activities
// src/activities.ts — non-deterministic code (I/O, APIs)
export async function fetchNpmData(packageName: string) {
const response = await fetch(`https://registry.npmjs.org/${packageName}`)
if (!response.ok) return null
const data = await response.json()
return {
name: data.name,
version: data["dist-tags"].latest,
description: data.description,
downloads: 0, // Fetched separately
}
}
export async function updateDatabase(
packageName: string,
version: string,
data: any
) {
await db.query(
`INSERT INTO packages (name, version, description, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (name) DO UPDATE SET version = $2, description = $3, updated_at = NOW()`,
[packageName, version, data.description]
)
}
export async function sendNotification(userId: string, message: string) {
await fetch("https://api.example.com/notify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId, message }),
})
}
export async function publishToRegistry(packageName: string, version: string) {
// Simulate publishing:
console.log(`Publishing ${packageName}@${version}`)
await new Promise((r) => setTimeout(r, 5000))
}
Worker and client
// src/worker.ts — runs activities and workflows
import { Worker } from "@temporalio/worker"
import * as activities from "./activities"
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve("./workflows"),
activities,
taskQueue: "package-management",
})
await worker.run()
}
run()
// src/client.ts — start and interact with workflows
import { Client } from "@temporalio/client"
import { publishPackageWorkflow, approvalSignal, statusQuery } from "./workflows"
const client = new Client()
// Start workflow:
const handle = await client.workflow.start(publishPackageWorkflow, {
taskQueue: "package-management",
workflowId: `publish-react-19.1.0`,
args: ["react", "19.1.0", "user-123"],
})
console.log(`Started workflow: ${handle.workflowId}`)
// Query status:
const status = await handle.query(statusQuery)
console.log(`Status: ${status}`)
// Send approval signal:
await handle.signal(approvalSignal, true)
// Wait for result:
const result = await handle.result()
console.log(`Result:`, result)
// Schedule recurring workflow:
await client.schedule.create({
scheduleId: "daily-package-sync",
spec: { intervals: [{ every: "6h" }] },
action: {
type: "startWorkflow",
workflowType: "packageSyncWorkflow",
taskQueue: "package-management",
args: [["react", "vue", "svelte", "angular"]],
},
})
Restate
Restate — durable execution engine:
Setup
npm install @restatedev/restate-sdk
# Start Restate server:
docker run -d --name restate -p 8080:8080 -p 9070:9070 docker.io/restatedev/restate:latest
Services and handlers
// src/services/packageService.ts
import * as restate from "@restatedev/restate-sdk"
const packageService = restate.service({
name: "packageService",
handlers: {
// Durable function — automatically retried, journaled:
async publishPackage(
ctx: restate.Context,
request: { name: string; version: string; publisherId: string }
) {
const { name, version, publisherId } = request
// Step 1: Fetch npm data (side effect — journaled):
const npmData = await ctx.run("fetch-npm", async () => {
const res = await fetch(`https://registry.npmjs.org/${name}`)
return res.json()
})
// Step 2: Update database (side effect):
await ctx.run("update-db", async () => {
await db.query(
`INSERT INTO packages (name, version, description, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (name) DO UPDATE SET version = $2, updated_at = NOW()`,
[name, version, npmData.description]
)
})
// Step 3: Send notification:
await ctx.run("notify", async () => {
await fetch("https://api.example.com/notify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId: publisherId,
message: `${name}@${version} published!`,
}),
})
})
return { success: true, publishedAt: new Date().toISOString() }
},
// Workflow with sleep (durable timer):
async scheduledSync(ctx: restate.Context, packages: string[]) {
for (const pkg of packages) {
const data = await ctx.run(`fetch-${pkg}`, async () => {
const res = await fetch(`https://registry.npmjs.org/${pkg}`)
return res.json()
})
await ctx.run(`update-${pkg}`, async () => {
await db.query(
"UPDATE packages SET version = $1 WHERE name = $2",
[data["dist-tags"].latest, pkg]
)
})
// Durable sleep — survives restarts:
await ctx.sleep(2000)
}
},
},
})
export default packageService
Virtual objects (stateful entities)
// src/services/packageTracker.ts
import * as restate from "@restatedev/restate-sdk"
const packageTracker = restate.object({
name: "packageTracker",
handlers: {
// Each package gets its own virtual object (keyed by package name):
async recordDownload(ctx: restate.ObjectContext, count: number) {
// Get current state:
const current = (await ctx.get<number>("totalDownloads")) ?? 0
const newTotal = current + count
// Update state (durable):
ctx.set("totalDownloads", newTotal)
// Check milestone:
if (current < 1000000 && newTotal >= 1000000) {
await ctx.run("milestone-notification", async () => {
await fetch("https://api.example.com/notify", {
method: "POST",
body: JSON.stringify({
message: `${ctx.key} hit 1M downloads!`,
}),
})
})
}
return { totalDownloads: newTotal }
},
async getStats(ctx: restate.ObjectSharedContext) {
const totalDownloads = (await ctx.get<number>("totalDownloads")) ?? 0
return { package: ctx.key, totalDownloads }
},
},
})
export default packageTracker
Server and client
// src/app.ts — register services
import * as restate from "@restatedev/restate-sdk"
import packageService from "./services/packageService"
import packageTracker from "./services/packageTracker"
restate
.endpoint()
.bind(packageService)
.bind(packageTracker)
.listen(9080)
// Register with Restate server:
// curl http://localhost:9070/deployments -H 'content-type: application/json' \
// -d '{"uri": "http://localhost:9080"}'
// Client — call services:
import * as restate from "@restatedev/restate-sdk-clients"
const client = restate.connect({ url: "http://localhost:8080" })
// Call service handler:
const result = await client
.serviceClient(packageService)
.publishPackage({
name: "react",
version: "19.1.0",
publisherId: "user-123",
})
// Call virtual object:
const stats = await client
.objectClient(packageTracker, "react")
.getStats()
// Send (fire and forget):
await client
.objectSendClient(packageTracker, "react")
.recordDownload(50000)
Windmill
Windmill — developer-first workflow platform:
Setup
# Docker Compose:
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
docker compose up -d
# Access: http://localhost:8000
Scripts (building blocks)
// Script: "fetch_npm_data" (TypeScript)
// Runs as a standalone script or workflow step
export async function main(packageName: string): Promise<{
name: string
version: string
description: string
downloads: number
}> {
// Fetch package info:
const registryRes = await fetch(`https://registry.npmjs.org/${packageName}`)
const registry = await registryRes.json()
// Fetch download count:
const downloadsRes = await fetch(
`https://api.npmjs.org/downloads/point/last-week/${packageName}`
)
const downloads = await downloadsRes.json()
return {
name: registry.name,
version: registry["dist-tags"].latest,
description: registry.description,
downloads: downloads.downloads,
}
}
// Script: "update_package_db"
import { Client } from "pg"
export async function main(
pgResource: object, // Windmill resource (connection details)
packageData: {
name: string
version: string
description: string
downloads: number
}
) {
const client = new Client(pgResource)
await client.connect()
await client.query(
`INSERT INTO packages (name, version, description, downloads, updated_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (name) DO UPDATE SET
version = $2, description = $3, downloads = $4, updated_at = NOW()`,
[packageData.name, packageData.version, packageData.description, packageData.downloads]
)
await client.end()
return { updated: packageData.name }
}
Flow (workflow)
// Flow: "package_sync_flow" (defined in Windmill UI or YAML)
// Steps are connected scripts
// Step 1: Fetch package list
// Script: inline
export async function main() {
return {
packages: ["react", "vue", "svelte", "angular", "next", "nuxt"],
}
}
// Step 2: For-each loop (parallel)
// Iterator: results.packages
// Script: "fetch_npm_data"
// Input: { packageName: flow_input.iter.value }
// Step 3: Update database (for each result)
// Script: "update_package_db"
// Input: { pgResource: $res:pg_production, packageData: results[1] }
// Step 4: Send summary
// Script: inline
export async function main(
slackResource: object,
results: Array<{ updated: string }>
) {
await fetch(slackResource.webhook_url, {
method: "POST",
body: JSON.stringify({
text: `📦 Synced ${results.length} packages: ${results.map((r) => r.updated).join(", ")}`,
}),
})
}
Approval flows
// Script with approval step:
import * as wmill from "windmill-client"
export async function main(
packageName: string,
version: string,
requesterId: string
) {
// Fetch data:
const res = await fetch(`https://registry.npmjs.org/${packageName}`)
const data = await res.json()
// Request approval (pauses workflow):
const approved = await wmill.getResumeUrls("approval")
// Send approval request:
await fetch("https://api.example.com/notify", {
method: "POST",
body: JSON.stringify({
to: "admin@example.com",
message: `Approve publish of ${packageName}@${version}?`,
approveUrl: approved.approvalPage,
}),
})
// Workflow pauses here until approved/rejected
// Windmill provides a UI for approval
return { approved: true, package: packageName, version }
}
Schedules and webhooks
# Schedule via CLI:
wmill schedule create \
--path "f/package-sync/package_sync_flow" \
--schedule "0 */6 * * *" \
--args '{}' \
--summary "Sync packages every 6 hours"
# Webhook trigger:
# Each script/flow gets a webhook URL:
# POST https://windmill.example.com/api/w/workspace/jobs/run/f/package-sync/package_sync_flow
# Headers: Authorization: Bearer <token>
# Body: { "packages": ["react", "vue"] }
// Windmill client API:
import * as wmill from "windmill-client"
// Run a script:
const jobId = await wmill.runScript({
path: "f/package-sync/fetch_npm_data",
args: { packageName: "react" },
})
// Get result:
const result = await wmill.waitJob(jobId)
console.log(result)
// Run a flow:
const flowJobId = await wmill.runFlow({
path: "f/package-sync/package_sync_flow",
args: {},
})
Feature Comparison
| Feature | Temporal | Restate | Windmill |
|---|---|---|---|
| Language | TypeScript/Go/Java/Python | TypeScript/Java/Kotlin | TypeScript/Python/Go/Bash |
| Execution model | Workflows + Activities | Services + Virtual Objects | Scripts + Flows |
| Durability | ✅ (event sourcing) | ✅ (journaling) | ✅ (job queue) |
| Automatic retries | ✅ | ✅ | ✅ |
| Durable timers | ✅ (sleep) | ✅ (sleep) | ✅ (schedule) |
| Signals/events | ✅ (signals + queries) | Via virtual objects | Via webhooks |
| State management | Workflow state | Virtual object state | Variables |
| Approval flows | Via signals | Custom | ✅ (built-in) |
| Visual builder | ❌ | ❌ | ✅ (flow editor) |
| Scheduling | ✅ (schedules) | Custom | ✅ (cron) |
| UI dashboard | ✅ (Temporal UI) | ✅ (dashboard) | ✅ (full IDE) |
| Self-hosted | ✅ | ✅ | ✅ |
| Cloud hosted | ✅ (Temporal Cloud) | ✅ (Restate Cloud) | ✅ (Windmill Cloud) |
| Complexity | High (powerful) | Medium | Low (scripts) |
| Scale | Enterprise | Service-level | Team/mid-size |
| License | MIT | BSL | AGPLv3 |
When to Use Each
Use Temporal if:
- Building mission-critical workflows that must never lose state
- Need complex workflow patterns (signals, queries, child workflows, sagas)
- Want the most battle-tested durable execution platform
- Building enterprise-scale orchestration with Go/Java/TypeScript
Use Restate if:
- Want lightweight durable execution without heavy infrastructure
- Need virtual objects for stateful entity management
- Prefer SDK-native approach (durable functions in your existing services)
- Building microservices that need reliable communication
Use Windmill if:
- Want a visual flow builder with script-based workflow steps
- Need approval flows, schedules, and webhook triggers out of the box
- Prefer a platform where scripts are the building blocks
- Building internal automation with a full web IDE for editing workflows
Methodology
Download data from npm registry and GitHub (March 2026). Feature comparison based on @temporalio/client v1.x, @restatedev/restate-sdk v1.x, and Windmill v1.x.
Compare workflow tools and developer platforms on PkgPulse →