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

@loopstack/chat-example-workflow

v0.21.6

Published

An example chat workflow to interact with an LLM.

Downloads

935

Readme

@loopstack/chat-example-workflow

A module for the Loopstack AI automation framework.

This module provides an example workflow demonstrating how to build an interactive chat interface with an LLM.

Overview

The Chat Example Workflow shows how to create a conversational assistant that processes user messages and generates responses using an LLM. It demonstrates the core patterns for building chat-based applications in Loopstack.

By using this workflow as a reference, you'll learn how to:

  • Set up a system prompt using a Handlebars template file
  • Use wait: true to pause the workflow for user input
  • Process user messages through an LLM with ClaudeGenerateText
  • Create a message loop for continuous conversation
  • Configure custom UI widgets for user input
  • Save documents with the workflow repository

This example is useful for developers building chatbots, virtual assistants, or any conversational AI interface.

Installation

See SETUP.md for installation and setup instructions.

How It Works

Key Concepts

1. System Prompt Setup

The workflow begins with an @Initial method that saves a hidden system message. The message content is rendered from a Handlebars template file:

@Initial({ to: 'waiting_for_user' })
async setup() {
  await this.repository.save(
    ClaudeMessageDocument,
    { role: 'user', content: this.render(__dirname + '/templates/systemMessage.md') },
    { meta: { hidden: true } },
  );
}

The { meta: { hidden: true } } option ensures the system message is included in the LLM context but not displayed in the chat UI.

2. Waiting for User Input

The userMessage transition uses wait: true to pause the workflow and wait for external input. A Zod schema defines the expected payload type:

@Transition({ from: 'waiting_for_user', to: 'ready', wait: true, schema: z.string() })
async userMessage(payload: string) {
  await this.repository.save(ClaudeMessageDocument, { role: 'user', content: payload });
}

When the user sends a message, the payload is saved as a ClaudeMessageDocument and the workflow transitions to the ready state.

3. LLM Response Generation

When the workflow reaches the ready state, it calls the LLM to generate a response based on the full conversation history using messagesSearchTag:

@Transition({ from: 'ready', to: 'waiting_for_user' })
async llmTurn() {
  const result = await this.claudeGenerateText.call({
    claude: { model: 'claude-sonnet-4-6' },
    messagesSearchTag: 'message',
  });
  await this.repository.save(ClaudeMessageDocument, result.data!, { id: result.data!.id });
}

The messagesSearchTag: 'message' parameter retrieves all saved ClaudeMessageDocument entries as conversation context. The LLM response is saved and the workflow loops back to waiting_for_user.

4. Custom UI Widgets

The workflow defines a prompt input widget that is enabled when waiting for user input:

ui:
  widgets:
    - widget: prompt-input
      enabledWhen:
        - waiting_for_user
      options:
        transition: userMessage
        label: Send Message

The transition: userMessage connects the widget to the userMessage method, and enabledWhen controls when the input is active.

Workflow Class

The complete workflow class uses @InjectTool() to access the ClaudeGenerateText tool and extends BaseWorkflow:

import { z } from 'zod';
import { ClaudeGenerateText, ClaudeMessageDocument } from '@loopstack/claude-module';
import { BaseWorkflow, Initial, InjectTool, Transition, Workflow } from '@loopstack/common';

@Workflow({
  uiConfig: __dirname + '/chat.ui.yaml',
})
export class ChatWorkflow extends BaseWorkflow {
  @InjectTool() claudeGenerateText: ClaudeGenerateText;

  @Initial({ to: 'waiting_for_user' })
  async setup() {
    await this.repository.save(
      ClaudeMessageDocument,
      { role: 'user', content: this.render(__dirname + '/templates/systemMessage.md') },
      { meta: { hidden: true } },
    );
  }

  @Transition({ from: 'waiting_for_user', to: 'ready', wait: true, schema: z.string() })
  async userMessage(payload: string) {
    await this.repository.save(ClaudeMessageDocument, { role: 'user', content: payload });
  }

  @Transition({ from: 'ready', to: 'waiting_for_user' })
  async llmTurn() {
    const result = await this.claudeGenerateText.call({
      claude: { model: 'claude-sonnet-4-6' },
      messagesSearchTag: 'message',
    });
    await this.repository.save(ClaudeMessageDocument, result.data!, { id: result.data!.id });
  }
}

Dependencies

This workflow uses the following Loopstack modules:

  • @loopstack/common - Core framework functionality, BaseWorkflow, decorators
  • @loopstack/claude-module - Provides ClaudeGenerateText tool and ClaudeMessageDocument

About

Author: Jakob Klippel

License: MIT

Additional Resources