@loopstack/prompt-example-workflow
A module for the Loopstack AI automation framework.
This module provides an example workflow demonstrating how to integrate an LLM using a simple prompt pattern.
Overview
The Prompt Example Workflow shows the most basic way to call an LLM in Loopstack — using a simple text prompt. It generates a haiku about a user-provided subject.
By using this workflow as a reference, you’ll learn how to:
- Define workflow input arguments with a Zod schema and default values
- Use the
promptparameter for simple LLM calls - Render Handlebars template files with dynamic variables
- Manage workflow state via the state object passed through transitions
- Save LLM responses as documents using
this.documentStore.save
This example is the ideal starting point for developers new to LLM integration in Loopstack.
Installation
npm install @loopstack/prompt-example-workflowThen register the module in your app:
import { StudioApp } from '@loopstack/common';
import { PromptExampleModule, PromptWorkflow } from '@loopstack/prompt-example-workflow';
@StudioApp({
title: 'Prompt Example',
workflows: [PromptWorkflow],
})
@Module({
imports: [PromptExampleModule],
})
export class MyAppModule {}Set your Anthropic API key as an environment variable:
ANTHROPIC_API_KEY=sk-ant-...How It Works
Key Concepts
1. Workflow Input Schema
Define input parameters with default values using a Zod schema in the @Workflow decorator. The workflow class extends BaseWorkflow<TArgs, TState> with matching types:
@Workflow({
title: 'Simple Prompt Example (Write a haiku)',
schema: z.object({
subject: z.string().default('coffee'),
}),
})
export class PromptWorkflow extends BaseWorkflow<{ subject: string }, PromptState> {The start @Transition method receives the state and context. Access workflow args via ctx.args:
@Transition({ to: 'prompt_executed' })
async prompt(state: PromptState, ctx: RunContext): Promise<PromptState> {
const args = ctx.args as { subject: string };2. Simple Prompt Pattern
Use the prompt parameter for straightforward LLM calls without conversation history. The prompt content is rendered from a Handlebars template file with variables. Provider and model are passed at call time via { config: { ... } }:
@Transition({ to: 'prompt_executed' })
async prompt(state: PromptState, ctx: RunContext): Promise<PromptState> {
const args = ctx.args as { subject: string };
const result = await this.llmGenerateText.call(
{
prompt: this.render(__dirname + '/templates/prompt.md', { subject: args.subject }),
},
{ config: { provider: 'claude', model: 'claude-sonnet-4-6' } },
);
return { llmResult: result.data, llmMeta: result.metadata as LlmResultMeta | undefined };
}The this.render() method loads a Handlebars template and interpolates the provided variables.
3. Storing Results in State
Tool results are stored in the state object returned from transitions, making them available in subsequent transitions:
interface PromptState {
llmResult?: LlmGenerateTextResult;
llmMeta?: LlmResultMeta;
}4. Saving Documents in a Final Transition
The terminal @Transition saves the stored LLM result as a LlmMessageDocument:
@Transition({ from: 'prompt_executed', to: 'end' })
async respond(state: PromptState): Promise<unknown> {
await this.documentStore.save(LlmMessageDocument, state.llmResult!.message, {
meta: { response: state.llmResult!.response, provider: state.llmMeta!.provider },
});
return {};
}Workflow Class
The complete workflow class:
import { z } from 'zod';
import { BaseWorkflow, Transition, Workflow } from '@loopstack/common';
import type { RunContext } from '@loopstack/common';
import type { LlmGenerateTextResult, LlmResultMeta } from '@loopstack/llm-provider-module';
import { LlmGenerateTextTool, LlmMessageDocument } from '@loopstack/llm-provider-module';
interface PromptState {
llmResult?: LlmGenerateTextResult;
llmMeta?: LlmResultMeta;
}
@Workflow({
title: 'Simple Prompt Example (Write a haiku)',
description: 'An example workflow that demonstrates how to use a prompt to generate a haiku.',
schema: z.object({
subject: z.string().default('coffee'),
}),
})
export class PromptWorkflow extends BaseWorkflow<{ subject: string }, PromptState> {
constructor(private readonly llmGenerateText: LlmGenerateTextTool) {
super();
}
@Transition({ to: 'prompt_executed' })
async prompt(state: PromptState, ctx: RunContext): Promise<PromptState> {
const args = ctx.args as { subject: string };
const result = await this.llmGenerateText.call(
{
prompt: this.render(__dirname + '/templates/prompt.md', { subject: args.subject }),
},
{ config: { provider: 'claude', model: 'claude-sonnet-4-6' } },
);
return { llmResult: result.data, llmMeta: result.metadata as LlmResultMeta | undefined };
}
@Transition({ from: 'prompt_executed', to: 'end' })
async respond(state: PromptState): Promise<unknown> {
await this.documentStore.save(LlmMessageDocument, state.llmResult!.message, {
meta: { response: state.llmResult!.response, provider: state.llmMeta!.provider },
});
return {};
}
}Dependencies
This workflow uses the following Loopstack modules:
@loopstack/common- Core framework functionality,BaseWorkflow, decorators@loopstack/llm-provider-module- ProvidesLlmGenerateTextTooltool,LlmGenerateTextResulttype, andLlmMessageDocument
About
Author: Jakob Klippel
License: MIT
Additional Resources
- Loopstack Documentation
- Getting Started with Loopstack
- Find more Loopstack examples in the Loopstack Registry