Skip to Content

Chat Flows

Build multi-turn conversational workflows with Claude using message documents and the messagesSearchTag pattern.

Example

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 }); } }

YAML Config

title: 'Chat Assistant' ui: widgets: - widget: prompt-input enabledWhen: - waiting_for_user options: transition: userMessage

How Message Accumulation Works

  1. All messages are saved as ClaudeMessageDocument — automatically tagged with message
  2. messagesSearchTag: 'message' tells claudeGenerateText to collect all documents with that tag as conversation history
  3. Each new message adds to the conversation — the LLM sees the full history on every turn

Chat Loop Flow

setup → waiting_for_user → [user sends message] → ready → llmTurn → waiting_for_user (loop)
  1. @Initial — Create system message (hidden from UI)
  2. Workflow enters waiting_for_user — UI shows the prompt-input widget
  3. User sends message → userMessage fires, saves user message as document
  4. llmTurn fires — calls Claude with full message history, saves response
  5. Workflow returns to waiting_for_user — loop continues

Combining with Tool Calling

Add tool calling to a chat flow by combining the patterns from AI Tool Calling:

@Transition({ from: 'ready', to: 'prompt_executed' }) async llmTurn() { const result = await this.claudeGenerateText.call({ claude: { model: 'claude-sonnet-4-6' }, messagesSearchTag: 'message', tools: ['getWeather', 'searchDatabase'], }); this.llmResult = result.data; } @Transition({ from: 'prompt_executed', to: 'awaiting_tools', priority: 10 }) @Guard('hasToolCalls') async executeToolCalls() { ... } @Transition({ from: 'prompt_executed', to: 'waiting_for_user' }) async respond() { await this.repository.save(ClaudeMessageDocument, this.llmResult!, { id: this.llmResult!.id }); }

Registry References

Last updated on