Skip to main content

Overview

Stepkit supports checkpoint-based workflows that can pause at any step, store their state, and resume later. This is perfect for human-in-the-loop workflows like approval systems, review queues, or multi-stage processes.

How Checkpoints Work

Checkpoints capture the complete state of a pipeline at a specific step, including:
  • All context data accumulated so far
  • The pipeline structure and remaining steps
  • The exact position in the execution
You can serialize checkpoints to a database, then resume from that exact point with optional data overrides.

Basic Checkpoint Usage

Checkpoints are emitted via the onStepComplete callback during pipeline execution.
import { stepkit } from 'stepkit'

const calc = stepkit<{ a: number; b?: number }>()
  .step('add-one', ({ a }) => ({ a: a + 1 }))
  .step('double', ({ a }) => ({ a: a * 2 }))
  .step('finish', ({ a, b }) => ({ sum: (a ?? 0) + (b ?? 0) }))

let checkpoint = ''
await calc.run(
  { a: 1 },
  {
    onStepComplete: (e) => {
      if (e.stepName === 'double') checkpoint = e.checkpoint
    },
  },
)

// Resume later with an override
const resumed = await calc.runCheckpoint({ checkpoint, overrideData: { b: 10 } })
Checkpoints are Base64-encoded strings containing the serialized pipeline state. Store them in a database, cache, or queue system.

Human Approval Flow

This example shows a complete approval workflow where an AI generates a draft email, pauses for human approval, then sends it when approved.
import { stepkit } from 'stepkit'

// Mocks
const kv: Record<string, string> = {}
const save = async (id: string, cp: string) => (kv[id] = cp)
const get = async (id: string) => kv[id] ?? null
const del = async (id: string) => { delete kv[id] }
const sendEmail = async ({ to, body }: { to: string; body: string }) => {
  console.log('Sending email to', to, 'with body:', body)
}

const replyFlow = stepkit<{ body: string }>()
  .step('generate', async ({ body }) => ({ reply: `Reply: ${body}` }))
  .step('send', async ({ reply }) => {
    await sendEmail({ to: '[email protected]', body: reply })
  })

export const start = async (body: string) => {
  let approvalId: string | null = null
  await replyFlow.run(
    { body },
    {
      async onStepComplete(e) {
        if (e.stepName.endsWith('generate')) {
          approvalId = `apr_${Date.now()}`
          await save(approvalId, e.checkpoint)
          e.stopPipeline()
        }
      },
    },
  )
  return { approvalId }
}

export const approve = async (approvalId: string) => {
  const checkpoint = await get(approvalId)
  if (!checkpoint) throw new Error('Not found')
  await replyFlow.runCheckpoint(checkpoint)
  await del(approvalId)
}

export const reject = async (approvalId: string) => {
  await del(approvalId)
}

Implementation Details

1

Generate Content

The generate step creates the draft using AI and returns it in the context.
2

Pause for Approval

When generate completes, onStepComplete saves the checkpoint and calls e.stopPipeline() to halt execution.
3

Store Checkpoint

The checkpoint is stored in a key-value store (database, Redis, etc.) with a unique approval ID.
4

Resume or Reject

When approved, runCheckpoint resumes from the saved state. If rejected, the checkpoint is simply deleted.

Key Methods

Callback fired after each step completes. Receives an event object with:
  • stepName: The name of the completed step
  • checkpoint: Base64 checkpoint string
  • stopPipeline(): Method to halt execution immediately
  • context: Current context data
Resumes a pipeline from a checkpoint. Accepts:
  • checkpoint: The checkpoint string to resume from
  • overrideData (optional): Shallow merge additional data into the context
Call this in onStepComplete to stop execution after the current step. The pipeline returns the current context without running remaining steps.

Real-World Use Cases

Content Review

Generate blog posts or social media content, pause for editorial review, publish when approved

Payment Approval

Process orders up to a threshold automatically, require manager approval for large amounts

Customer Support

AI drafts responses, human agents review and approve before sending to customers

Data Changes

Stage database migrations or configuration changes, require approval before applying

Best Practices

Don’t store sensitive data in checkpoints if you’re using external storage. Consider encrypting checkpoints or using reference IDs for sensitive information.
Set expiration times on stored checkpoints to automatically clean up abandoned approval requests.

Checkpoint Storage

Choose a storage backend based on your requirements:
  • Redis: Fast, in-memory, with TTL support
  • Database: Persistent, queryable, with audit trails
  • S3/Object Storage: For long-term retention or large contexts
  • Job Queue: Built-in checkpoint support in systems like BullMQ

Error Handling

Always handle missing or invalid checkpoints:
const checkpoint = await get(approvalId)
if (!checkpoint) {
  throw new Error('Approval request expired or not found')
}

Override Data

When resuming, you can override context data. This is useful for:
  • Adding human review comments
  • Updating timestamps
  • Modifying generated content before continuing
await pipeline.runCheckpoint({
  checkpoint,
  overrideData: {
    reviewedBy: '[email protected]',
    reviewedAt: new Date().toISOString(),
    approvalNotes: 'Looks good!',
  },
})

AI Workflows

Combine approvals with AI generation workflows

Content Moderation

Use branching logic before requesting approval

Build docs developers (and LLMs) love