Skip to main content
Wire Type: Workflow

Durable workflows,
plain TypeScript.

wireWorkflow orchestrates multi-step processes with durable execution, retries, sleep, and deterministic replay — all in plain TypeScript.

The Problem

The problem with workflow engines today

Existing tools solve durable execution well — but they come with trade-offs that compound as your system grows.

Separate infrastructure

Temporal requires a cluster. Inngest needs their cloud or a self-hosted server. You're adding another service to manage, monitor, and pay for — before you write a single workflow.

New programming model

Temporal has activities, workflows, signals, and queries. Inngest has step.run, step.sleep, step.sendEvent. Each comes with its own SDK, its own patterns, its own testing story. Your existing functions can't be reused.

Vendor lock-in

Your workflow logic is coupled to the engine. Switching from Inngest to Temporal means rewriting everything. Your business logic lives inside their SDK.

Pikku takes a different approach. Your workflows use the same functions you already have, run on your existing infrastructure, and persist state to any storage backend. No new service. No new SDK. No lock-in.

How It Compares

Pikku vs the alternatives

Temporal, Inngest, and Mastra are excellent tools. Pikku's advantage is that workflows use the same functions and infrastructure you already have, with no separate service.

Pikku
TemporalInngestMastra
SetupYour existing appSeparate cluster + workersCloud service or self-hostYour existing app
Step definitionworkflow.do() with your functionsActivities + workflow codestep.run() callbacksstep() with z.object()
Function reuseSame Pikku functionsWrap in activitiesInline or importInline or import
Sleep / timersworkflow.sleep()workflow.sleep()step.sleep()Not built-in
State persistenceAny storage backendTemporal serverInngest cloudIn-memory or custom
TypeScript-nativeYes, plain TSSDK with decoratorsYesYes
Graph workflowsBuilt-in DAGsManual orchestrationFan-out onlyBuilt-in step graphs
AI agent integrationSame frameworkSeparate concernSeparate concernSame framework

Use Cases

What you can build

Each step is a regular Pikku function — the same ones you use for HTTP, queues, and everything else.

User onboarding

createProfileaddToCRMsleep(5min)sendWelcomeEmail

Payment processing

createChargewaitForWebhookgenerateInvoicesendReceipt

Content pipeline

generateDrafthumanReviewpublishPostnotifySubscribers

Data migration

exportFromSourcetransformRecordsimportToTargetverifyIntegrity

The Basics

Orchestrate with workflow.do()

Each workflow.do() call is a durable step. RPC steps run as queue jobs. Inline steps execute immediately. Both are cached for replay.

onboarding.workflow.ts
const onboardUser = pikkuWorkflowFunc<
{ email: string; userId: string },
{ success: boolean }
>(async ({}, data, { workflow }) => {
// Step 1: RPC call (executed as queue job)
const user = await workflow.do(
'Create profile',
'createUserProfile',
{ email: data.email, userId: data.userId }
)

// Step 2: Inline step (immediate, cached)
const message = await workflow.do(
'Generate welcome',
async () => `Welcome, ${data.email}!`
)

// Step 3: Durable sleep
await workflow.sleep('Wait 5 minutes', '5min')

// Step 4: Another RPC call
await workflow.do('Send email', 'sendEmail', {
to: data.email,
subject: 'Welcome!',
body: message,
})

return { success: true }
})

Deterministic replay

Completed steps are never re-executed. Results are cached and replayed on recovery.

Plain TypeScript

Loops, conditionals, Promise.all — use any TypeScript construct. No YAML, no DSL.

Typed I/O

Workflow input and output are fully typed. Each RPC step infers types from the target function.

Step Types

Four step primitives

Every workflow is built from these four building blocks.

workflow.do(name, rpcName, data, options?)

RPC

Execute a Pikku function as a queue job. Supports retries and retry delay.

workflow.do(name, async () => value)

inline

Inline step — runs immediately, result cached for replay. Great for transformations.

workflow.sleep(name, duration)

sleep

Durable sleep. Survives restarts — the workflow resumes after the duration.

workflow.suspend(reason)

suspend

Pause the workflow until explicitly resumed. For human-in-the-loop approval flows.

Patterns

Fan-out, retry, branch

Use standard TypeScript for control flow. Promise.all for parallelism. if/else for branching. Retries via step options.

workflow-patterns.ts
// Fan-out: parallel steps with Promise.all
const users = await Promise.all(
data.userIds.map(async (userId) =>
await workflow.do(`Get user ${userId}`, 'userGet', { userId })
)
)

// Retry: automatic retry with backoff
const payment = await workflow.do(
'Process payment',
'processPayment',
{ amount: 100 },
{ retries: 3, retryDelay: '1s' }
)

// Branch: standard TypeScript conditionals
if (user.plan === 'pro') {
await workflow.do('Apply discount', 'applyDiscount', { userId })
}

Graph Workflows

Declarative DAGs

For node-based workflows, use pikkuWorkflowGraph. Define nodes, edges, and input mappings — Pikku handles execution order and parallelism.

onboarding.graph.tsgraph
const userOnboarding = pikkuWorkflowGraph({
description: 'Onboard a new user',
nodes: {
createProfile: 'createUserProfile',
sendWelcome: 'sendEmail',
setupDefaults: 'createDefaultTodos',
},
config: {
createProfile: {
next: ['sendWelcome', 'setupDefaults'], // Parallel
},
sendWelcome: {
input: (ref) => ({
to: ref('createProfile', 'email'),
subject: 'Welcome!',
}),
},
},
})

Branching

Use graph.branch('key') inside a node to select which edge to follow. Record-based next config maps keys to nodes.

ref() for data flow

Use ref('nodeId', 'path') to reference output from previous nodes — resolved at runtime.

HTTP Wiring

Start, poll, resume

Pikku provides helper functions to wire workflows to HTTP endpoints — start, run to completion, or poll status.

workflow.wiring.ts
// Start a workflow (returns runId)
wireHTTP({
method: 'post',
route: '/workflow/onboard',
func: workflowStart('userOnboarding'),
})

// Run to completion (synchronous)
wireHTTP({
method: 'post',
route: '/workflow/onboard/run',
func: workflow('userOnboarding'),
})

// Check status
wireHTTP({
method: 'get',
route: '/workflow/status/:runId',
func: workflowStatus('userOnboarding'),
})

Start wiring workflows in 5 minutes

One command to scaffold a project with workflow wiring already configured.

$ npm create pikku@latest

MIT Licensed  ·  DSL & graph workflows