Skip to content

Node.js Quick Start

Terminal window
pnpm add sayiir

Define tasks with task() and chain them with flow():

import { task, flow, runWorkflow } from "sayiir";
const fetchUser = task("fetch-user", (userId: number) => {
return { id: userId, name: "Alice" };
});
const sendEmail = task("send-email", (user: { id: number; name: string }) => {
return `Sent welcome to ${user.name}`;
});
const workflow = flow<number>("welcome")
.then(fetchUser)
.then(sendEmail)
.build();
const result = await runWorkflow(workflow, 42);
console.log(result); // "Sent welcome to Alice"

The generic flow<number> tells TypeScript the workflow input type. Each .then() infers the output type and passes it to the next step — full type safety with zero annotations.

Use runDurableWorkflow to checkpoint after each task. If the process crashes, call resumeWorkflow to continue from the last checkpoint.

import { task, flow, runDurableWorkflow, InMemoryBackend } from "sayiir";
const processOrder = task("process-order", (orderId: number) => {
return { orderId, status: "processed" };
}, { timeout: "30s" });
const sendConfirmation = task("send-confirmation", (order: { orderId: number }) => {
return `Confirmed order ${order.orderId}`;
});
const workflow = flow<number>("order")
.then(processOrder)
.then(sendConfirmation)
.build();
const backend = new InMemoryBackend();
// Checkpoints after each task — resume from last checkpoint on crash
const status = runDurableWorkflow(workflow, "order-123", 42, backend);
if (status.status === "completed") {
console.log(status.output); // "Confirmed order 42"
}

Swap to PostgresBackend for production — everything else stays the same:

import { PostgresBackend } from "sayiir";
// Connects and runs migrations automatically
const backend = PostgresBackend.connect(process.env.DATABASE_URL!);
const status = runDurableWorkflow(workflow, "order-123", 42, backend);

Sayiir integrates with Zod as an optional peer dependency for input and output validation:

import { z } from "zod";
import { task, flow, runWorkflow } from "sayiir";
const OrderSchema = z.object({
orderId: z.string(),
amount: z.number().positive(),
});
const processOrder = task("process-order", (order) => {
// order is validated and typed as { orderId: string; amount: number }
return { status: "ok", message: `Processed $${order.amount}` };
}, {
input: OrderSchema,
});
const workflow = flow("typed").then(processOrder).build();
const result = await runWorkflow(workflow, { orderId: "1", amount: 99.99 });

Install Zod separately: pnpm add zod.

You don’t need task() for every step — pass inline functions directly:

const workflow = flow<number>("quick")
.then("double", (x) => x * 2)
.then("format", (x) => `Result: ${x}`)
.build();
const result = await runWorkflow(workflow, 21);
// "Result: 42"

Use task() when you need metadata (retries, timeouts, tags) or want to reuse a task across workflows.

Use .fork() and .join() to run branches in parallel:

import { task, flow, branch, runWorkflow } from "sayiir";
const validatePayment = task("validate-payment", (order) => ({ payment: "valid" }));
const checkInventory = task("check-inventory", (order) => ({ stock: "available" }));
const workflow = flow<{ id: number }>("checkout")
.fork([
branch("payment", validatePayment),
branch("inventory", checkInventory),
])
.join("finalize", ([payment, inventory]) => {
return { ...payment, ...inventory };
})
.build();
const result = await runWorkflow(workflow, { id: 1 });