packages/cli/skills/pikku-workflow/SKILL.md
Use when building multi-step workflows, state machines, or orchestration pipelines with Pikku. Covers pikkuWorkflowFunc, workflow steps (do, sleep, suspend), graph workflows, and HTTP wiring. TRIGGER when: code uses pikkuWorkflowFunc/pikkuWorkflowGraph, user asks about workflows, multi-step processes, durable execution, suspend/resume, or DAG orchestration. DO NOT TRIGGER when: user asks about simple background jobs (use pikku-queue) or scheduled tasks (use pikku-cron).
npx skillsauth add pikkujs/pikku pikku-workflowInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill as an execution checklist, not reference material.
pikku-meta when available; otherwise run the relevant pikku meta ... --json command and inspect only the focused output you need..pikku, node_modules, vendored packages, or broad build artifacts.pikku-verify or pikku all when functions, wirings, schemas, or generated clients may have changed.Build durable, multi-step workflows with automatic retry, sleep, suspend/resume, and parallel execution. Steps are cached for replay safety.
pikku info functions --verbose # See existing functions that can be workflow steps
pikku info tags --verbose # Understand project organization
See pikku-concepts for the core mental model.
pikkuWorkflowFunc<TInput, TOutput>(fn)import { pikkuWorkflowFunc } from '#pikku'
const myWorkflow = pikkuWorkflowFunc<InputType, OutputType>(
async (services, data, { workflow }) => {
// workflow.do(), workflow.sleep(), workflow.suspend()
return result
}
)
// RPC step — execute a Pikku function as a queue job
// workflow.do(stepName, funcName, data, options?)
const result = await workflow.do(
'Create profile',
'createUserProfile',
{
email: data.email,
},
{ retries: 3, retryDelay: '1s' }
)
// Inline step — immediate execution, cached for replay
// workflow.do(stepName, asyncFn)
const result = await workflow.do('Generate message', async () => {
return `Welcome, ${data.email}!`
})
// Sleep — durable pause (duration: '5min', '1h', '30s', '1d')
await workflow.sleep('Wait 5 minutes', '5min')
// Suspend — pause until externally resumed
await workflow.suspend('Awaiting approval')
pikkuWorkflowGraph(config) — DAG Workflowsimport { pikkuWorkflowGraph } from '#pikku'
pikkuWorkflowGraph({
description: 'Onboard a new user',
nodes: {
createProfile: 'createUserProfile', // nodeName → Pikku function name
sendWelcome: 'sendEmail',
},
config: {
createProfile: {
next: ['sendWelcome'], // Nodes to run after this one (parallel)
},
sendWelcome: {
input: (ref) => ({
// Transform input using refs to prior node outputs
to: ref('createProfile', 'email'),
subject: 'Welcome!',
}),
},
},
})
// Start a workflow
wireHTTP({
method: 'post',
route: '/workflow/onboard',
func: workflowStart('workflowName'),
})
// Execute workflow steps (called by orchestrator)
wireHTTP({
method: 'post',
route: '/workflow/onboard/run',
func: workflow('workflowName'),
})
// Check workflow status
wireHTTP({
method: 'get',
route: '/workflow/status/:runId',
func: workflowStatus('workflowName'),
})
const onboardUser = pikkuWorkflowFunc<
{ email: string; userId: string },
{ success: boolean }
>(async ({}, data, { workflow }) => {
const user = await workflow.do('Create profile', 'createUserProfile', {
email: data.email,
userId: data.userId,
})
const message = await workflow.do(
'Generate welcome',
async () => `Welcome, ${data.email}!`
)
await workflow.sleep('Wait 5 minutes', '5min')
await workflow.do('Send email', 'sendEmail', {
to: data.email,
subject: 'Welcome!',
body: message,
})
return { success: true }
})
const users = await Promise.all(
data.userIds.map(
async (userId) =>
await workflow.do(`Get user ${userId}`, 'userGet', { userId })
)
)
const payment = await workflow.do(
'Process payment',
'processPayment',
{ amount: 100 },
{ retries: 3, retryDelay: '1s' }
)
if (user.plan === 'pro') {
await workflow.do('Apply discount', 'applyDiscount', { userId })
}
const approval = pikkuWorkflowFunc<
{ requestId: string },
{ approved: boolean }
>(async ({}, data, { workflow }) => {
await workflow.do('Submit request', 'submitRequest', data)
await workflow.suspend('Awaiting approval')
// Workflow pauses here until externally resumed
const result = await workflow.do('Check result', 'getApprovalResult', data)
return { approved: result.approved }
})
const userOnboarding = pikkuWorkflowGraph({
description: 'Onboard a new user',
nodes: {
createProfile: 'createUserProfile',
sendWelcome: 'sendEmail',
setupDefaults: 'createDefaultTodos',
},
config: {
createProfile: {
next: ['sendWelcome', 'setupDefaults'], // Run in parallel
},
sendWelcome: {
input: (ref) => ({
to: ref('createProfile', 'email'),
subject: 'Welcome!',
}),
},
},
})
// functions/onboarding.workflow.ts
export const onboardUser = pikkuWorkflowFunc<
{ email: string; userId: string; plan: string },
{ success: boolean }
>(async ({}, data, { workflow }) => {
// Step 1: Create user profile
const user = await workflow.do('Create profile', 'createUserProfile', {
email: data.email,
userId: data.userId,
})
// Step 2: Set up defaults based on plan
if (data.plan === 'pro') {
await workflow.do('Apply pro features', 'enableProFeatures', {
userId: data.userId,
})
}
// Step 3: Send welcome email
await workflow.do('Send welcome', 'sendEmail', {
to: data.email,
subject: 'Welcome!',
body: `Welcome to our platform, ${user.name}!`,
})
// Step 4: Wait then send follow-up
await workflow.sleep('Wait 1 day', '1d')
await workflow.do('Send follow-up', 'sendEmail', {
to: data.email,
subject: 'Getting started',
body: 'Here are some tips to get started...',
})
return { success: true }
})
// wirings/workflow.wiring.ts
wireHTTP({
method: 'post',
route: '/onboard',
func: workflowStart('onboardUser'),
})
wireHTTP({
method: 'post',
route: '/onboard/run',
func: workflow('onboardUser'),
})
wireHTTP({
method: 'get',
route: '/onboard/status/:runId',
func: workflowStatus('onboardUser'),
})
documentation
Deprecated — use pikku-middleware instead. Tag middleware (addTagMiddleware) is now documented as a section within the pikku-middleware skill, alongside global HTTP middleware, execution order, and the service-to-service bearer auth pattern.
testing
Use when adding authorization checks to Pikku functions or routes — pikkuPermission, pikkuAuth, per-function permissions, pattern-based permissions, or understanding OR/AND permission logic. TRIGGER when: user wants to restrict who can call a function, check resource ownership, add role-based access, or understand where permission checks belong. DO NOT TRIGGER when: user asks about middleware or request interception (use pikku-middleware), authentication strategies (use pikku-security), or session management.
testing
Use when adding any middleware to a Pikku app — global HTTP middleware, tag-scoped middleware (including service-to-service bearer auth), per-route middleware, session-setting middleware, or understanding middleware execution order and priority. TRIGGER when: user wants middleware on some or all routes, machine-to-machine auth, tag-scoped cross-cutting concerns, global interceptors, or middleware priority/order questions. DO NOT TRIGGER when: user asks about permissions/authorization checks (use pikku-permissions), auth strategies like authBearer/authCookie (use pikku-security), or deployment.
documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.