npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@swizzy_ai/kit

v1.0.7

Published

A TypeScript library for building wizard-like workflows with LLM integration

Readme

Wizard Framework

A Framework for Designing LLM Experiences

npm version License: MIT

InstallDocumentationExamples


Why Wizard Exists

The Current State is Broken

Today's LLM orchestration assumes the LLM itself is the agent - an intelligent entity you hand tools to and hope it figures things out. This paradigm works brilliantly in controlled environments (see: Claude Code's success), but fails catastrophically when:

  • The environment isn't perfectly curated
  • Mistakes or hallucinations have real costs
  • Context limits constrain what the model can see
  • Each turn summons a "new spirit" with no memory of the last

The mental model is fundamentally flawed.

What Wizard Believes

Real agency and intelligence are emergent products of process and design.

Wizard inverts the paradigm: the developer is the orchestrator, the LLM is the user. You design the experience - what information to present, what to collect, what happens next. The LLM focuses on what it does best: generating the most plausible text for each scenario.

This is agentic Lego bricks, not autonomous agents.


Installation

npm install @swizzy_ai/kit

API Key Configuration

For multimodal functions and different LLM providers, set all your API keys as environment variables:

# Include multiple API keys
OPENAI_API_KEY="your-openai-key"
ANTHROPIC_API_KEY="your-anthropic-key"
GEMINI_API_KEY="your-gemini-key"

Or configure them programmatically:

const { Wizard, Models } = require('@swizzy_ai/kit');

const wizard = new Wizard({
  id: 'my-workflow',
});

Core Concepts

Understanding Wizard's Architecture

Wizard workflows operate on a simple execution loop:

Step → LLM Generation → Update Function → Signal → Next Step

You, the developer, design each step in this loop:

  • What the LLM sees (via context functions)
  • What the LLM generates (via instructions and schemas)
  • What happens next (via signals)

The LLM never decides where to go or what to do - it only generates text based on what you show it and where to tell it. You orchestrate the intelligence.


State and Context Management

Wizard uses two complementary concepts for data management:

State: The Global Data Store

State is the persistent, shared data that flows through your entire workflow. It's the "source of truth" that all steps can read from and write to.

// Set initial state
wizard.setState({
  userQuestion: 'What are the key findings?',
  documents: [
    { title: 'Q1 Report', pages: 45, data: [...] },
    { title: 'Q2 Report', pages: 52, data: [...] }
  ],
  processedCount: 0
});

// State is available in every update function
wizard.addComputeStep({
  id: 'check_status',
  update: (result, state, actions) => {
    console.log(`Processed ${state.processedCount} documents`);
    return actions.next();
  }
});

// Update state (supports both object and functional updates)
wizard.addComputeStep({
  id: 'increment_counter',
  update: (result, state, actions) => {
    actions.setState({ processedCount: state.processedCount + 1 });
    // Or using functional updates:
    // actions.setState(prevState => ({ processedCount: prevState.processedCount + 1 }));
    return actions.next();
  }
});

Context: What the LLM Sees

Context is a function that transforms your global state into a focused, LLM-friendly view. This is where you design the information architecture for each step.

The context function receives the full state and returns only what the LLM should see for that specific step.

wizard.addTextStep({
  id: 'analyze',
  instruction: 'Analyze these documents:\n{{formattedDocs}}',
  context: (state) => ({
    // Transform global state into LLM-friendly format
    formattedDocs: state.documents
      .map((doc, i) => `${i + 1}. ${doc.title} (${doc.pages} pages)`)
      .join('\n'),
    // Only expose what's needed for this step
    totalDocs: state.documents.length
  }),
  schema: z.object({
    insights: z.array(z.string())
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, state, actions) => {
    actions.setState({ insights: result.insights });
    return actions.next();
  }
});

Why Context Functions Matter:

  • Token Efficiency: Show only relevant data to stay within LLM limits
  • Information Architecture: Present data in the optimal format for the task
  • Security: Prevent LLMs from seeing sensitive or distracting information
  • UX Design: Craft the exact "interface" each LLM step experiences

Key Distinction:

  • State = Global data store (persistent, shared, updatable)
  • Context = Per-step data transformation (temporary, focused, read-only for LLM)

Migration Note: updateContext(), setContext(), and getContext() are still supported for backward compatibility, but setState() and getState() are the preferred modern APIs. setState() supports both object and functional updates.


Steps: The Building Blocks

A Step is the fundamental unit of a Wizard workflow. Each step defines a single action in your process.

Step Anatomy

Every step has five components:

  1. ID: Unique identifier for the step
  2. Type: What kind of step (text, structured, or compute)
  3. Instruction: What to ask the LLM (for LLM steps)
  4. Context Function: What information to expose (optional)
  5. Update Function: How to handle the result and what to do next

Step Variants

Choose the right step type for your task:

Normal Step (Structured Data)

Use when you need the LLM to generate data matching a specific schema:

wizard.addStep({
  id: 'extract_entities',
  instruction: 'Extract entities from: {{text}}',
  schema: z.object({
    people: z.array(z.string()),
    organizations: z.array(z.string()),
    locations: z.array(z.string())
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    // result is guaranteed to match schema
    actions.setState({ entities: result });
    return actions.next();
  }
});

Text Step (Simple Text)

Use when you just need a text response without structured data:

wizard.addTextStep({
  id: 'summarize',
  instruction: 'Summarize: {{document}}',
  model: Models.SWIZZY_DEFAULT,
  update: (text, state, actions) => {
    actions.setState({ summary: text });
    return actions.next();
  }
});

Compute Step (Non-LLM Logic)

Use for calculations, API calls, validation, or any logic that doesn't need an LLM:

wizard.addComputeStep({
  id: 'validate',
  update: (result, context, actions) => {
    if (context.entities.people.length === 0) {
      return actions.retry(); // No entities found, retry
    }
    
    // Call external API
    const enrichedData = await fetch(`/api/enrich`, {
      method: 'POST',
      body: JSON.stringify(context.entities)
    });
    
    actions.updateContext({ enrichedData });
    return actions.next();
  }
});

Instructions: Prompt Templates

The instruction is the prompt shown to the LLM. It uses a template engine that replaces {{variables}} with data from your context function.

Basic Template Syntax

wizard.addTextStep({
  id: 'greet_user',
  instruction: 'Greet {{userName}} who has {{documentCount}} documents',
  context: (context) => ({
    userName: context.user.name,
    documentCount: context.user.documents.length
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (text, context, actions) => {
    console.log(text); // "Hello Alice! I see you have 5 documents."
    return actions.next();
  }
});

At runtime, the instruction becomes:

"Greet Alice who has 5 documents"

Multi-line Instructions

wizard.addStep({
  id: 'analyze_sentiment',
  instruction: `
You are analyzing customer feedback.

Customer Review:
{{review}}

Previous Sentiment: {{previousSentiment}}

Analyze the sentiment and confidence level.
  `.trim(),
  context: (context) => ({
    review: context.currentReview.text,
    previousSentiment: context.lastSentiment || 'unknown'
  }),
  schema: z.object({
    sentiment: z.enum(['positive', 'negative', 'neutral']),
    confidence: z.number().min(0).max(1)
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    actions.updateContext({ 
      lastSentiment: result.sentiment,
      sentimentConfidence: result.confidence
    });
    return actions.next();
  }
});

Update Functions: State Mutation and Control

The update function runs after a step completes. It's where you:

  1. Process the LLM's output
  2. Update context/state
  3. Decide what happens next (via signals)

Function Signature

update: (result, context, actions) => {
  // result: The output from this step
  // context: Current workflow state (read-only)
  // actions: Methods to update state and control flow
  
  return actions.next(); // Must return a signal
}

Processing Results

wizard.addStep({
  id: 'extract_data',
  instruction: 'Extract key data from {{document}}',
  schema: z.object({
    title: z.string(),
    date: z.string(),
    amount: z.number()
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    // Validate the result
    if (result.amount < 0) {
      console.error('Invalid amount detected');
      return actions.retry();
    }
    
    // Transform the result
    const enrichedData = {
      ...result,
      parsedDate: new Date(result.date),
      formattedAmount: `$${result.amount.toFixed(2)}`
    };
    
    // Update context
    actions.updateContext({ extractedData: enrichedData });
    
    // Continue
    return actions.next();
  }
});

Workflow Control: Signals

What Are Signals?

Signals are how steps communicate "what happens next" in your workflow. Every update function must return a signal.

Think of signals as the control flow language of Wizard - they let you build complex branching logic, retry mechanisms, and dynamic routing.


Available Signals

actions.next()

Move to the next sequential step in your workflow.

wizard.addComputeStep({
  id: 'step_1',
  update: (result, context, actions) => {
    return actions.next(); // Goes to step_2
  }
});

wizard.addComputeStep({
  id: 'step_2',
  update: (result, context, actions) => {
    return actions.next(); // Goes to step_3
  }
});

Use when: Following a linear, predictable flow.


actions.goto(stepId)

Jump to a specific step by its ID.

wizard.addComputeStep({
  id: 'router',
  update: (result, context, actions) => {
    if (context.needsValidation) {
      return actions.goto('validate_step');
    }
    
    if (context.needsEnrichment) {
      return actions.goto('enrich_step');
    }
    
    return actions.goto('finalize_step');
  }
});

wizard.addComputeStep({
  id: 'validate_step',
  update: (result, context, actions) => {
    // Validation logic
    return actions.next();
  }
});

wizard.addComputeStep({
  id: 'enrich_step',
  update: (result, context, actions) => {
    // Enrichment logic
    return actions.next();
  }
});

Use when: Implementing conditional branching or complex routing logic.


actions.retry()

Re-run the current step.

wizard.addStep({
  id: 'extract_with_retry',
  instruction: 'Extract entities from: {{text}}',
  schema: z.object({
    entities: z.array(z.string()),
    confidence: z.number()
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    // Retry if confidence is too low
    if (result.confidence < 0.8) {
      const retryCount = context.retryCount || 0;
      
      if (retryCount < 3) {
        actions.updateContext({ retryCount: retryCount + 1 });
        return actions.retry();
      }
    }
    
    // Success or max retries reached
    actions.updateContext({ finalEntities: result.entities });
    return actions.next();
  }
});

Use when: The LLM output needs improvement or validation failed.

Pro tip: Track retry counts to avoid infinite loops.


actions.stop()

End the workflow immediately.

wizard.addComputeStep({
  id: 'check_completion',
  update: (result, context, actions) => {
    if (context.allDocumentsProcessed) {
      console.log('Workflow complete!');
      return actions.stop(); // End here
    }
    
    return actions.next(); // Continue processing
  }
});

Use when: Early termination, error conditions, or goal achievement.


actions.wait()

Pause for 10 seconds before continuing to the next step.

wizard.addComputeStep({
  id: 'rate_limit_handler',
  update: (result, context, actions) => {
    if (context.apiCallsThisMinute >= 60) {
      console.log('Rate limit reached, waiting...');
      actions.updateContext({ apiCallsThisMinute: 0 });
      return actions.wait(); // Pause 10 seconds
    }
    
    return actions.next();
  }
});

Use when: Rate limiting, waiting for external systems, or pacing execution.


Signal Patterns

Sequential Execution

wizard.addTextStep({
  id: 'step_1',
  instruction: 'Do task 1',
  model: Models.SWIZZY_DEFAULT,
  update: (text, context, actions) => {
    actions.updateContext({ task1Result: text });
    return actions.next();
  }
});

wizard.addTextStep({
  id: 'step_2',
  instruction: 'Do task 2 using {{task1Result}}',
  model: Models.SWIZZY_DEFAULT,
  update: (text, context, actions) => {
    return actions.next();
  }
});

Conditional Routing

wizard.addComputeStep({
  id: 'router',
  update: (result, context, actions) => {
    if (context.complexity === 'simple') {
      return actions.goto('simple_path');
    } else if (context.complexity === 'medium') {
      return actions.goto('medium_path');
    } else {
      return actions.goto('complex_path');
    }
  }
});

Retry with Backoff

wizard.addStep({
  id: 'api_call_with_retry',
  instruction: 'Process: {{data}}',
  schema: z.object({ success: z.boolean() }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    if (!result.success) {
      const retries = context.retries || 0;
      
      if (retries < 3) {
        actions.updateContext({ retries: retries + 1 });
        return actions.wait(); // Wait before retry
      }
      
      return actions.goto('error_handler');
    }
    
    return actions.next();
  }
});

Loop Pattern

wizard.addComputeStep({
  id: 'process_items',
  update: (result, context, actions) => {
    const currentIndex = context.currentIndex || 0;
    
    if (currentIndex < context.items.length) {
      actions.updateContext({ 
        currentItem: context.items[currentIndex],
        currentIndex: currentIndex + 1
      });
      return actions.goto('process_single_item');
    }
    
    // All items processed
    return actions.next();
  }
});

wizard.addTextStep({
  id: 'process_single_item',
  instruction: 'Process: {{currentItem}}',
  model: Models.SWIZZY_DEFAULT,
  update: (text, context, actions) => {
    // Store result
    actions.updateContext({
      [`result_${context.currentIndex}`]: text
    });
    // Loop back
    return actions.goto('process_items');
  }
});

Advanced Patterns

The Bungee Action: Parallelism

The Bungee Action is Wizard's most powerful pattern. It implements fan-out/fan-in: launch a step multiple times in parallel, then automatically return to the anchor point.

Anatomy of a Bungee

  • Anchor Step: Where the bungee launches from (typically a compute step)
  • Destination Step: The step that runs in parallel for each item
  • Batch: Each parallel run receives unique context
  • Fan-in: All parallel runs complete, execution returns to the anchor
  • Continuation: The anchor's next signal determines where to go
graph TD
    A[Anchor Step] --> B[🪂 Bungee Launch]
    B --> C[Worker 1: Item A]
    B --> D[Worker 2: Item B]
    B --> E[Worker N: Item Z]
    C --> F[All Complete]
    D --> F
    E --> F
    F --> G[Return to Anchor]
    G --> H[Continue Flow]

Basic Bungee Example

wizard.setState({
  documents: ['doc1.txt', 'doc2.txt', 'doc3.txt']
});

// Anchor step
wizard.addComputeStep({
  id: 'parallel_processor',
  update: (result, context, actions) => {
    return actions.bungee.init()
      .batch(
        'process_document',           // Destination step
        context.documents.length,     // Run 3 times
        (index) => ({                 // Each run gets unique context
          documentName: context.documents[index],
          documentIndex: index
        })
      )
      .config({ 
        concurrency: 2,  // Process 2 at a time
        timeout: 30000   // 30 second timeout per worker
      })
      .jump(); // Launch!
  }
});

// Destination step (runs 3 times in parallel)
wizard.addTextStep({
  id: 'process_document',
  instruction: 'Summarize document: {{documentName}}',
  schema: z.object({
    summary: z.string(),
    wordCount: z.number()
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    // Store result with unique key
    actions.updateContext({
      [`summary_${context.documentIndex}`]: result.summary,
      [`wordCount_${context.documentIndex}`]: result.wordCount
    });
    return actions.next(); // Return to anchor
  }
});

// Back at anchor - collect results
wizard.addComputeStep({
  id: 'collect_results',
  update: (result, context, actions) => {
    const allSummaries = context.documents.map((_, i) => 
      context[`summary_${i}`]
    );
    
    actions.updateContext({ allSummaries });
    return actions.next();
  }
});

Real-World Example: Parallel Document Search

Search 100 pages simultaneously instead of sequentially:

wizard.setState({
  userQuestion: 'What are the key findings about climate change?',
  totalPages: 100
});

// Anchor: Launch parallel search
wizard.addComputeStep({
  id: 'parallel_search',
  update: (result, context, actions) => {
    return actions.bungee.init()
      .batch(
        'search_page',
        context.totalPages,         // 100 parallel searches
        (pageIndex) => ({
          pageNumber: pageIndex + 1,
          query: context.userQuestion
        })
      )
      .config({ 
        concurrency: 10,  // 10 workers at a time
        timeout: 30000
      })
      .jump();
  }
});

// Worker: Search a single page
wizard.addTextStep({
  id: 'search_page',
  instruction: 'Search page {{pageNumber}} for information about: {{query}}',
  schema: z.object({
    hasRelevantInfo: z.boolean(),
    excerpt: z.string().optional(),
    relevanceScore: z.number().min(0).max(10).optional()
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    if (result.hasRelevantInfo && result.relevanceScore >= 7) {
      actions.updateContext({
        [`page_${context.pageNumber}_excerpt`]: result.excerpt,
        [`page_${context.pageNumber}_score`]: result.relevanceScore
      });
    }
    return actions.next(); // Return to anchor
  }
});

// Collect and rank results
wizard.addComputeStep({
  id: 'collect_and_rank',
  update: (result, context, actions) => {
    const results = [];
    
    for (let i = 1; i <= context.totalPages; i++) {
      const excerpt = context[`page_${i}_excerpt`];
      const score = context[`page_${i}_score`];
      
      if (excerpt) {
        results.push({ page: i, excerpt, score });
      }
    }
    
    // Sort by relevance
    results.sort((a, b) => b.score - a.score);
    
    actions.updateContext({ 
      searchResults: results.slice(0, 10) // Top 10
    });
    
    return actions.next();
  }
});

Performance: Process 100 pages in ~30 seconds (with concurrency: 10) instead of 5+ minutes sequentially.


When to Use Bungee

Perfect for:

  • Batch processing (documents, images, data records)
  • Parallel API calls or searches
  • Multi-source information gathering
  • Any workflow bottleneck that can be parallelized

Not ideal for:

  • Tasks that must run sequentially
  • When order matters
  • Shared state that can't be partitioned

Bungee Configuration Options

actions.bungee.init()
  .batch('destination_step', count, contextFn)
  .config({
    concurrency: 10,    // Max parallel workers (default: 5)
    timeout: 30000,     // Timeout per worker in ms (default: 60000)
    retryOnError: true  // Retry failed workers (default: false)
  })
  .jump();

Multi-API Configuration

Wizard supports multiple LLM providers. Configure API keys for different models:

const wizard = new Wizard({
  id: 'multi-provider-workflow',
  apiKeys: {
    openai: process.env.OPENAI_API_KEY,
    anthropic: process.env.ANTHROPIC_API_KEY,
    gemini: process.env.GEMINI_API_KEY
  }
});

// Use different models for different tasks
wizard.addTextStep({
  id: 'fast_extraction',
  instruction: 'Quick extract: {{data}}',
  model: Models.GPT_4O_MINI,  // Fast, cheap
  update: (text, context, actions) => {
    actions.updateContext({ extracted: text });
    return actions.next();
  }
});

wizard.addTextStep({
  id: 'deep_analysis',
  instruction: 'Deeply analyze: {{extracted}}',
  model: Models.CLAUDE_SONNET,  // Powerful reasoning
  update: (text, context, actions) => {
    actions.updateContext({ analysis: text });
    return actions.next();
  }
});

Complete Example

Here's a full workflow that demonstrates all concepts:

const { Wizard, Models } = require('@swizzy_ai/kit');
const { z } = require('zod');

const wizard = new Wizard({
  id: 'document-analyzer',
  onUsage: (usage, provider) => {
    console.log(`📊 ${provider}: ${usage.totalTokens} tokens`);
  }
});

// Initialize state
wizard.setState({
  documentText: 'Your document content here...',
  userQuery: 'Extract key insights',
  maxRetries: 3
});

// Step 1: Extract entities with retry logic
wizard.addStep({
  id: 'extract_entities',
  instruction: 'Extract named entities from: {{documentText}}',
  schema: z.object({
    people: z.array(z.string()),
    organizations: z.array(z.string()),
    confidence: z.number().min(0).max(1)
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    const retries = context.extractRetries || 0;
    
    // Low confidence? Retry
    if (result.confidence < 0.8 && retries < context.maxRetries) {
      actions.updateContext({ extractRetries: retries + 1 });
      return actions.retry();
    }
    
    // Store entities
    actions.updateContext({ 
      entities: result,
      extractRetries: 0
    });
    return actions.next();
  }
});

// Step 2: Validate entities
wizard.addComputeStep({
  id: 'validate_entities',
  update: (result, context, actions) => {
    const hasEntities = 
      context.entities.people.length > 0 ||
      context.entities.organizations.length > 0;
    
    if (!hasEntities) {
      console.log('No entities found, retrying extraction');
      return actions.goto('extract_entities');
    }
    
    console.log(`Found ${context.entities.people.length} people, ${context.entities.organizations.length} orgs`);
    return actions.next();
  }
});

// Step 3: Generate summary
wizard.addTextStep({
  id: 'generate_summary',
  instruction: `
Summarize key insights about these entities:

{{entityList}}

Focus on their relationships and significance.
  `.trim(),
  context: (context) => ({
    entityList: [
      ...context.entities.people.map(p => `Person: ${p}`),
      ...context.entities.organizations.map(o => `Organization: ${o}`)
    ].join('\n')
  }),
  schema: z.object({
    summary: z.string(),
    keyInsights: z.array(z.string()),
    confidence: z.number()
  }),
  model: Models.SWIZZY_DEFAULT,
  update: (result, context, actions) => {
    console.log('Summary:', result.summary);
    console.log('Key Insights:');
    result.keyInsights.forEach((insight, i) => {
      console.log(`  ${i + 1}. ${insight}`);
    });
    
    return actions.stop(); // Workflow complete!
  }
});

// Run the workflow
wizard.run();

Events

Wizard emits events throughout execution for monitoring and integration. Listen using wizard.on(eventName, callback).

State Events

| Event | Description | Data Structure | |-------|-------------|----------------| | state:update | Fired when state is modified | { previousState, newState, updates, timestamp } |

Example:

wizard.on('state:update', (data) => {
  console.log('State changed:', data.updates);
  console.log('New state:', data.newState);
});

Wizard Lifecycle Events

| Event | Description | Data Structure | |-------|-------------|----------------| | start | Wizard execution begins | { wizardId, timestamp, steps } | | complete | Wizard execution finishes successfully | { duration, totalSteps, timestamp } | | wizard:stop | Wizard execution stops (completion or manual) | { reason, finalState, timestamp } | | pause | Wizard execution is paused | { timestamp, currentStepId } | | resume | Wizard execution resumes | { timestamp, currentStepId } |

Step Events

| Event | Description | Data Structure | |-------|-------------|----------------| | step:start | Step begins execution | { stepId, instruction, timestamp } | | step:complete | Step finishes successfully | { stepId, data, duration, timestamp } | | step:failed | Step fails permanently | { stepId, error, retryCount, timestamp } | | step:error | Step encounters error (may retry) | { stepId, error, retryCount, timestamp } | | step:retry | Step is being retried | { stepId, attempt, error, timestamp } |

Streaming Events

| Event | Description | Data Structure | |-------|-------------|----------------| | step:chunk | Raw text chunk during streaming | { stepId, chunk, timestamp } | | step:streaming | Parsed data chunk during streaming | { stepId, data, timestamp } |

Example - Complete Event Monitoring:

// Monitor all step progress
wizard.on('step:start', (data) => {
  console.log(`🚀 Starting step: ${data.stepId}`);
});

wizard.on('step:complete', (data) => {
  console.log(`✅ Completed step: ${data.stepId} (${data.duration}ms)`);
  console.log('Result:', data.data);
});

wizard.on('step:failed', (data) => {
  console.error(`❌ Failed step: ${data.stepId}`, data.error);
});

// Monitor wizard completion
wizard.on('wizard:stop', (data) => {
  console.log(`🏁 Wizard finished (${data.reason})`);
  console.log('Final state:', data.finalState);
});

Example - Real-time Streaming:

wizard.on('step:streaming', (data) => {
  // Update UI with partial results
  updateUI(data.stepId, data.data);
});

wizard.on('step:chunk', (data) => {
  // Handle raw text streaming
  appendToOutput(data.chunk);
});

API Reference

Wizard Constructor

new Wizard(config: {
  id: string;
  systemPrompt?: string;
  onUsage?: (usage: TokenUsage, provider: string) => void;
  apiKeys?: {
    openai?: string;
    anthropic?: string;
    gemini?: string;
  }
})

Context Methods

| Method | Description | |--------|-------------| | setState(data) | Initialize workflow state | | getState() | Retrieve current state | | setState(updates) | Update state (supports object or functional updates) | | getContext() | Retrieve current state (legacy alias for getState) |

Step Methods

| Method | Description | |--------|-------------| | addStep(config) | Add structured LLM step with schema validation | | addTextStep(config) | Add simple text generation step | | addComputeStep(config) | Add non-LLM logic step |

Execution Methods

| Method | Description | |--------|-------------| | run() | Execute the workflow from the first step | | stop() | Manually stop a running workflow |

Actions Interface

Available in every update function:

{
  setState: (updates: object | function) => void;
  updateContext: (updates: object) => void; // Legacy
  next: () => FlowControlSignal;
  goto: (stepId: string) => FlowControlSignal;
  retry: () => FlowControlSignal;
  stop: () => FlowControlSignal;
  wait: () => FlowControlSignal;
  bungee: {
    init: () => BungeeBuilder;
  }
}

Bungee Builder Interface

BungeeBuilder {
  batch: (
    destinationStepId: string,
    count: number,
    context: (index: number) => object
  ) => BungeeBuilder;
  
  config: (options: {
    concurrency?: number;
    timeout?: number;
    retryOnError?: boolean;
  }) => BungeeBuilder;
  
  jump: () => FlowControlSignal;
}

Comparison with Alternatives

| Feature | Wizard | LangChain | Vercel AI SDK | Raw LLM APIs | |---------|---------|-----------|---------------|--------------| | Mental Model | Developer orchestrates, LLM executes | LLM is the agent | Linear chat flows | Full manual control | | Context Control | ✅ Context functions + templates | ⚠️ Chain-based passing | ❌ Manual | ❌ Manual | | State Machine | ✅ Explicit signals (next/goto/retry/stop) | ❌ Sequential chains | ❌ Linear only | ❌ Build yourself | | Type Safety | ✅ Zod validation on outputs | ⚠️ Optional | ⚠️ TypeScript only | ❌ None | | Parallelism | ✅ Bungee actions | ❌ Manual orchestration | ❌ Manual | ❌ Manual | | Flow Control | ✅ Signals with branching | ⚠️ Limited | ⚠️ Limited | ❌ Manual | | Learning Curve | Medium | Steep | Low | High | | Best For | Complex multi-step workflows | General orchestration | Simple chat | Full control |


Examples

Check the /examples directory for complete workflows:

  • Document Analyzer: Extract entities and generate insights with retry logic
  • Parallel Search: Search 100+ pages simultaneously using Bungee actions
  • Multi-Step Validation: Complex validation with conditional routing
  • Data Pipeline: Transform and validate data across multiple steps
  • Multi-Provider Workflow: Use different LLM providers for different tasks

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

# Setup
git clone https://github.com/swizzy-ai/wizard-framework.git
npm install

# Development
npm test        # Run tests
npm run build   # Build TypeScript
npm run lint    # Lint code

License

MIT License - see LICENSE for details.