.claude/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.
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
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.
development
Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to "support RTL / right-to-left / bidi / mirror the layout", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.
development
Wire i18n into a Pikku frontend (Vite SPA, Vite SSR, or Next.js app-router) with react-i18next + i18next. English by default, every user-facing string goes through a `t()` token, and additional languages are served under `/de` `/es` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to "make this translatable / use tokens / add i18n". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.
development
Use when translating an n8n Code node body into a real Pikku function body. Triggered when the user opens or points at a stub generated by @pikku/n8n-import (look for `STUB — generated from n8n Code node` in the file's JSDoc), or when the user says 'translate this n8n code', 'port this n8n code node', 'finish the codeStub__... function', etc. The stub file is a `pikkuSessionlessFunc` with a Zod input/output, a JSDoc preserving the original n8n JavaScript verbatim, and a `throw new Error('… — implement me')` body.