Chat Flows
Build multi-turn conversational workflows with Claude using message documents and the messagesSearchTag pattern.
Example
import { z } from 'zod';
import { LlmGenerateTextTool, LlmMessageDocument } from '@loopstack/llm-provider-module';
import { BaseWorkflow, Initial, InjectTool, Transition, Workflow } from '@loopstack/common';
@Workflow({ uiConfig: __dirname + '/chat.ui.yaml' })
export class ChatWorkflow extends BaseWorkflow {
@InjectTool({ provider: 'claude', model: 'claude-sonnet-4-6' }) llmGenerateText: LlmGenerateTextTool;
@Initial({ to: 'waiting_for_user' })
async setup() {
await this.repository.save(
LlmMessageDocument,
{ 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(LlmMessageDocument, { role: 'user', content: payload });
}
@Transition({ from: 'ready', to: 'waiting_for_user' })
async llmTurn() {
const result = await this.llmGenerateText.call({
model: 'claude-sonnet-4-6' },
messagesSearchTag: 'message',
});
await this.repository.save(LlmMessageDocument, result.data!, { id: result.data!.id });
}
}YAML Config
title: 'Chat Assistant'
ui:
widgets:
- widget: prompt-input
enabledWhen:
- waiting_for_user
options:
transition: userMessageHow Message Accumulation Works
- All messages are saved as
LlmMessageDocument— automatically tagged withmessage messagesSearchTag: 'message'tellsllmGenerateTextto collect all documents with that tag as conversation history- 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)@Initial— Create system message (hidden from UI)- Workflow enters
waiting_for_user— UI shows the prompt-input widget - User sends message →
userMessagefires, saves user message as document llmTurnfires — calls Claude with full message history, saves response- 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:
// Configure tools on @InjectTool
@InjectTool({ provider: 'claude', model: 'claude-sonnet-4-6', tools: ['getWeather', 'searchDatabase'] })
llmGenerateText: LlmGenerateTextTool;
@Transition({ from: 'ready', to: 'prompt_executed' })
async llmTurn() {
const result = await this.llmGenerateText.call({
messagesSearchTag: 'message',
});
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(LlmMessageDocument, this.llmResult!.message, {
meta: { response: this.llmResult!.response, provider: 'claude' },
});
}Registry References
- chat-example-workflow — Multi-turn chat with Claude, system message, and prompt-input widget
- tool-call-example-workflow — Chat with tool calling loop
Last updated on