DocumentationReference

HazelJS Prompts Package

npm downloads

@hazeljs/prompts provides centralized, versioned prompt management for AI applications — typed templates, a global registry, and file/Redis/database storage.

Quick Reference

  • Purpose: @hazeljs/prompts manages LLM prompts as first-class application artifacts with typed templates, named variable placeholders, versioning, a global registry, and pluggable storage (file, Redis, database).
  • When to use: Use @hazeljs/prompts when a HazelJS application has multiple LLM prompts that need to be centrally managed, versioned, A/B tested, or overridden at runtime. Use inline string prompts for simple single-prompt applications.
  • Key concepts: Prompt templates with {{variable}} placeholders, PromptRegistry (global registry), PromptModule, prompt versioning, storage backends (file, Redis, database), synchronous render API, async persistence API.
  • Dependencies: @hazeljs/core.
  • Common patterns: Define prompt templates → register in PromptRegistry → render with variables → use with @hazeljs/ai or @hazeljs/agent system prompts.
  • Common mistakes: Scattering prompts in string constants across service files (hard to manage); not versioning prompts (can't roll back); not using typed variables (runtime errors from missing placeholders).

Why a Prompt Registry?

LLM prompts are first-class application logic. As applications grow, prompts end up scattered in string constants across service files, making them hard to review, version, A/B test, or override at runtime. @hazeljs/prompts solves this:

ProblemWithout @hazeljs/promptsWith @hazeljs/prompts
Scattered prompt stringsPrompts live inline with business logicCentralized in a typed registry
No typingstring everywherePromptTemplate<{ var1, var2 }>
No versioningEdit in place, lose historyVersioned entries, get(key, version)
Hard to overrideModify source codePromptRegistry.override(key, template) at startup
No persistenceLost on restart if loaded from DBFile / Redis / Database backends
Re-deploy to update promptsCode change requiredHot-swap from Redis or DB without restart

Architecture

graph TD
  A["Application Code"] -->|"register / override"| B["PromptRegistry<br/>(in-memory cache)"]
  B -->|"get(key)"| C["PromptTemplate<br/>.render(variables)"]
  C --> D["Rendered Prompt String"]
  B <-->|"save / loadAll / getAsync"| E["Store Backend(s)"]
  E --> F["MemoryStore"]
  E --> G["FileStore<br/>(JSON on disk)"]
  E --> H["RedisStore<br/>(Redis)"]
  E --> I["DatabaseStore<br/>(Prisma / Drizzle)"]
  E --> J["MultiStore<br/>(fan-out to all)"]

  style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style B fill:#6366f1,stroke:#818cf8,stroke-width:2px,color:#fff
  style C fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style D fill:#f59e0b,stroke:#fbbf24,stroke-width:2px,color:#fff
  style E fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff

Key Components

  1. PromptTemplate<TVariables> — A typed template with {variableName} placeholders. The type parameter enforces which variables must be supplied to .render().
  2. PromptRegistry — A global static store. Prompts are registered at import time and read synchronously. Optional store backends add persistence and versioning.
  3. Store Backends — Five implementations with a uniform PromptStore interface: MemoryStore, FileStore, RedisStore, DatabaseStore, MultiStore.

Installation

npm install @hazeljs/prompts

Optional dependencies based on store backends:

npm install ioredis          # For RedisStore
npm install @prisma/client   # For DatabaseStore with Prisma

Quick Start

1. Define and Register Templates

import { PromptTemplate, PromptRegistry } from '@hazeljs/prompts';

// Define a typed template
const ragAnswerPrompt = new PromptTemplate<{ context: string; question: string }>(
  `Answer the question using only the provided context. Be concise and factual.

Context:
{context}

Question: {question}

Answer:`,
  { name: 'RAG Answer', version: '1.0.0' },
);

// Register it under a namespaced key
PromptRegistry.register('rag:qa:answer', ragAnswerPrompt);

2. Render at Runtime

import { PromptRegistry } from '@hazeljs/prompts';

const prompt = PromptRegistry.get<{ context: string; question: string }>('rag:qa:answer');

const rendered = prompt.render({
  context: 'HazelJS is a TypeScript framework for building AI-native applications.',
  question: 'What is HazelJS?',
});

console.log(rendered);
// Answer the question using only the provided context. Be concise and factual.
//
// Context:
// HazelJS is a TypeScript framework for building AI-native applications.
//
// Question: What is HazelJS?
//
// Answer:

3. Override at Startup

Override any built-in prompt before the app processes requests — no code changes required:

import { PromptRegistry, PromptTemplate } from '@hazeljs/prompts';

// Replace the built-in RAG answer prompt with your fine-tuned version
PromptRegistry.override('rag:qa:answer', new PromptTemplate<{ context: string; question: string }>(
  `You are a helpful assistant. Use the context below to answer the question.
Context: {context}
Q: {question}
A:`,
  { name: 'Custom RAG Answer', version: '2.0.0' },
));

PromptTemplate

PromptTemplate<TVariables> is the core primitive. It holds a raw template string and metadata, and provides a single render() method.

import { PromptTemplate } from '@hazeljs/prompts';

// Simple untyped template
const simple = new PromptTemplate(
  'Summarize the following text in {maxWords} words: {text}',
  { name: 'Summarizer', version: '1.0.0' },
);

console.log(simple.render({ text: 'Long article...', maxWords: '50' }));

// Typed template — TypeScript enforces the variable shape
const typed = new PromptTemplate<{
  customerName: string;
  orderId: string;
  issue: string;
}>(
  `You are a customer support agent.
Customer: {customerName}
Order: {orderId}
Issue: {issue}

Respond empathetically and provide a solution.`,
  { name: 'Support Response', version: '1.0.0' },
);

// TypeScript error if variables are missing or misspelled
const rendered = typed.render({
  customerName: 'Alice',
  orderId: 'ORD-001',
  issue: 'Item arrived damaged',
});

Placeholder rules:

  • Placeholders are {variableName} — alphanumeric and underscores
  • Missing variables are left as-is (e.g. {missing} stays {missing}) — easy to spot during testing
  • Extra variables in the render() call are ignored

Enhanced Template Features

PromptTemplate now supports advanced templating features:

Strict Mode

Enforce that all placeholders have values:

const template = new PromptTemplate<{ name: string; age: number }>(
  'Hello {name}, you are {age} years old.',
  { name: 'Greeting', version: '1.0.0' }
);

// Throws error: Missing required template variable: "age"
template.render({ name: 'Alice' }, { strict: true });

Conditionals

Show/hide content based on variable truthiness:

const template = new PromptTemplate<{ premium?: boolean; name: string }>(
  `Hello {name}!
{#if premium}
You have access to premium features.
{/if}`,
  { name: 'Welcome', version: '1.0.0' }
);

template.render({ name: 'Alice', premium: true });
// Shows premium message

template.render({ name: 'Bob' });
// Hides premium message

Loops

Iterate over arrays:

const template = new PromptTemplate<{ items: string[] }>(
  `Shopping list:
{#each items}
- {.}
{/each}`,
  { name: 'List', version: '1.0.0' }
);

template.render({ items: ['Milk', 'Eggs', 'Bread'] });
// Shopping list:
// - Milk
// - Eggs
// - Bread

Use {@index} to access the current index:

const template = new PromptTemplate<{ tasks: string[] }>(
  `{#each tasks}
{@index}. {.}
{/each}`,
  { name: 'Numbered List', version: '1.0.0' }
);

Partials (Include)

Reuse prompts within other prompts:

// Register a reusable partial
PromptRegistry.register('common:signature', new PromptTemplate(
  'Best regards,\nThe Support Team',
  { name: 'Email Signature', version: '1.0.0' }
));

// Include it in another template
const emailTemplate = new PromptTemplate<{ customerName: string }>(
  `Dear {customerName},

Thank you for contacting us.

{@include common:signature}`,
  { name: 'Support Email', version: '1.0.0' }
);

// The registry automatically resolves {@include} directives
const rendered = PromptRegistry.get('support:email').render(
  { customerName: 'Alice' }
);

Introspection

Extract all variable names from a template:

const template = new PromptTemplate<{ name: string; items: string[] }>(
  'Hello {name}! {#each items}{.}{/each}',
  { name: 'Example', version: '1.0.0' }
);

const vars = template.variables();
console.log(vars); // ['name', 'items']

Preview Mode

Get a rendered preview with token estimates:

const preview = template.preview({ name: 'Alice', items: ['A', 'B'] });
console.log(preview);
// Hello Alice! AB
//
// --- Preview: ~3 tokens, 14 chars ---

Metadata Shape

interface PromptMetadata {
  name: string;         // Human-readable display name
  version: string;      // Semver string — used for versioned retrieval
  description?: string; // Optional description
  tags?: string[];      // Optional tags for categorization
}

PromptRegistry

PromptRegistry is a global static class — no instantiation needed. Prompts registered in one file are immediately available across the entire process.

Key Naming Convention

Use a colon-separated package:scope:action scheme to avoid collisions:

rag:graph:entity-extraction
rag:graph:community-summary
agent:supervisor:routing
agent:support:system-prompt
myapp:checkout:upsell

Sync API

import { PromptRegistry } from '@hazeljs/prompts';

// Register (no-op if key already exists — safe for built-in defaults)
PromptRegistry.register('myapp:qa:answer', myTemplate);

// Override (always replaces — use at startup for customisation)
PromptRegistry.override('myapp:qa:answer', customTemplate);

// Get latest version (throws if not registered)
const tpl = PromptRegistry.get('myapp:qa:answer');

// Get a specific version
const v1 = PromptRegistry.get('myapp:qa:answer', '1.0.0');

// Check existence
if (PromptRegistry.has('myapp:qa:answer')) { /* ... */ }

// List all registered keys
const keys = PromptRegistry.list();
console.log(keys); // ['rag:qa:answer', 'myapp:qa:answer', ...]

// List all cached versions for a key
const versions = PromptRegistry.versions('myapp:qa:answer');
console.log(versions); // ['1.0.0', '1.1.0', '2.0.0']

// Unregister (useful in tests)
PromptRegistry.unregister('myapp:qa:answer');

// Clear all (tests only)
PromptRegistry.clear();

Async Store API

When store backends are configured, use the async API to load from and persist to storage:

// Load from store if not in cache (falls back through configured stores in order)
const tpl = await PromptRegistry.getAsync('myapp:qa:answer');
const tplV2 = await PromptRegistry.getAsync('myapp:qa:answer', '2.0.0');

// Persist a single prompt to all configured stores
await PromptRegistry.save('myapp:qa:answer');

// Persist all registered prompts
await PromptRegistry.saveAll();

// Load all prompts from the primary store into the cache
await PromptRegistry.loadAll();

// Load and overwrite existing cache entries
await PromptRegistry.loadAll(true);

Store Backends

MemoryStore

In-memory store — useful for testing and for building an explicit in-process prompt library:

import { MemoryStore, PromptRegistry } from '@hazeljs/prompts';

const store = new MemoryStore();
PromptRegistry.configure([store]);

FileStore

Persists prompts to a JSON file on disk — useful for local development and single-server deployments:

import { FileStore, PromptRegistry } from '@hazeljs/prompts';

const store = new FileStore({ filePath: './prompts/library.json' });
PromptRegistry.configure([store]);

// Persist current registry to disk
await PromptRegistry.saveAll();

// Load prompts from disk on startup
await PromptRegistry.loadAll();

The JSON file format:

[
  {
    "key": "rag:qa:answer",
    "template": "Answer the question...",
    "metadata": { "name": "RAG Answer", "version": "1.0.0" }
  }
]

RedisStore

Stores prompts in Redis — ideal for multi-instance deployments where prompts need to be shared and hot-swapped:

import Redis from 'ioredis';
import { RedisStore, PromptRegistry } from '@hazeljs/prompts';

const redis = new Redis({ host: 'localhost', port: 6379 });

const store = new RedisStore({
  client: redis,
  keyPrefix: 'hazel:prompts:', // optional namespace (default: 'hazel:prompts:')
});

PromptRegistry.configure([store]);

// Load all prompts from Redis on startup
await PromptRegistry.loadAll();

// After updating a prompt, push it to Redis
PromptRegistry.override('rag:qa:answer', updatedTemplate);
await PromptRegistry.save('rag:qa:answer');

Hot-swap without restart:

// In a separate admin endpoint or script:
PromptRegistry.override('rag:qa:answer', newTemplate);
await PromptRegistry.save('rag:qa:answer');
// All processes sharing the same Redis will pick up the new version on the next getAsync() call.

DatabaseStore

Stores prompts in a relational database using any adapter that implements DatabaseAdapter:

import { DatabaseStore, PromptRegistry } from '@hazeljs/prompts';
import type { DatabaseAdapter, PromptEntry } from '@hazeljs/prompts';

// Implement the adapter for your ORM (Prisma example)
class PrismaPromptAdapter implements DatabaseAdapter {
  constructor(private readonly prisma: PrismaClient) {}

  async get(key: string, version?: string): Promise<PromptEntry | undefined> {
    const record = await this.prisma.prompt.findFirst({
      where: { key, ...(version ? { version } : {}) },
      orderBy: { createdAt: 'desc' },
    });
    if (!record) return undefined;
    return { key: record.key, template: record.template, metadata: JSON.parse(record.metadata) };
  }

  async set(entry: PromptEntry): Promise<void> {
    await this.prisma.prompt.upsert({
      where: { key_version: { key: entry.key, version: entry.metadata.version } },
      create: { key: entry.key, template: entry.template, metadata: JSON.stringify(entry.metadata) },
      update: { template: entry.template, metadata: JSON.stringify(entry.metadata) },
    });
  }

  async getAll(): Promise<PromptEntry[]> {
    const records = await this.prisma.prompt.findMany({ orderBy: { createdAt: 'desc' } });
    return records.map(r => ({ key: r.key, template: r.template, metadata: JSON.parse(r.metadata) }));
  }
}

const prisma = new PrismaClient();
const store = new DatabaseStore({ adapter: new PrismaPromptAdapter(prisma) });
PromptRegistry.configure([store]);

MultiStore

Fan-out store that writes to all configured backends simultaneously and reads from the first one that has a result. Use this for high-availability deployments (e.g. Redis as primary, database as fallback):

import { MultiStore, FileStore, RedisStore, PromptRegistry } from '@hazeljs/prompts';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

const store = new MultiStore([
  new RedisStore({ client: redis }),       // primary — fast reads
  new FileStore({ filePath: './prompts/library.json' }), // fallback — persisted to disk
]);

PromptRegistry.configure([store]);

// saveAll() writes to both stores
await PromptRegistry.saveAll();

// getAsync() reads from Redis first; falls back to file if Redis misses
const tpl = await PromptRegistry.getAsync('rag:qa:answer');

Configuring Multiple Stores

import { PromptRegistry, RedisStore, FileStore } from '@hazeljs/prompts';

// Replace all configured stores at once
PromptRegistry.configure([new RedisStore({ client: redis })]);

// Or append a store without replacing existing ones
PromptRegistry.addStore(new FileStore({ filePath: './backup.json' }));

// Inspect which stores are configured
console.log(PromptRegistry.storeNames()); // ['RedisStore', 'FileStore']

Integration with @hazeljs/agent

The @hazeljs/agent package uses @hazeljs/prompts internally to manage all system prompts — entity extraction, community summaries, supervisor routing, and more. Override any of them at startup to customise agent behaviour without forking:

import '';
import { PromptRegistry, PromptTemplate } from '@hazeljs/prompts';
import { AgentRuntime } from '@hazeljs/agent';

// Override the built-in supervisor routing prompt before creating the runtime
PromptRegistry.override(
  'agent:supervisor:routing',
  new PromptTemplate<{ task: string; workers: string }>(
    `You are a project manager. Decompose the task and assign each subtask to the best worker.
Available workers: {workers}
Task: {task}
Respond with a JSON array of { worker, subtask } objects.`,
    { name: 'Custom Supervisor Routing', version: '2.0.0' },
  ),
);

// Runtime picks up the overridden prompt automatically
const runtime = new AgentRuntime({ /* ... */ });

Integration with @hazeljs/rag

@hazeljs/rag registers its GraphRAG extraction and synthesis prompts under rag:graph:* keys. Override them to tune extraction quality for your domain:

import { PromptRegistry, PromptTemplate } from '@hazeljs/prompts';

// Tune entity extraction for a legal document corpus
PromptRegistry.override(
  'rag:graph:entity-extraction',
  new PromptTemplate<{ text: string }>(
    `Extract legal entities (parties, clauses, obligations, dates) from this text.
Return JSON: { entities: [...], relationships: [...] }

Text: {text}`,
    { name: 'Legal Entity Extraction', version: '1.0.0' },
  ),
);

Complete Example: Prompt-Driven Support Agent

import '';
import { PromptRegistry, PromptTemplate, FileStore } from '@hazeljs/prompts';
import { Tool, ToolRegistry } from '@hazeljs/agent';
import { createMcpServer } from '@hazeljs/mcp';

// ─── 1. Configure store backend ───────────────────────────────────────────────

PromptRegistry.configure([new FileStore({ filePath: './prompts/support.json' })]);
await PromptRegistry.loadAll();

// ─── 2. Register prompts ──────────────────────────────────────────────────────

PromptRegistry.register(
  'support:ticket:triage',
  new PromptTemplate<{ issue: string; customerTier: string }>(
    `Classify this support issue and suggest an urgency level.
Customer tier: {customerTier}
Issue: {issue}
Respond with JSON: { category, urgency: "low"|"medium"|"high", suggestedAction }`,
    { name: 'Ticket Triage', version: '1.0.0' },
  ),
);

// ─── 3. Use prompts inside tools ──────────────────────────────────────────────

class SupportAgent {
  @Tool({
    description: 'Triage a support issue and return category, urgency, and suggested action.',
    parameters: [
      { name: 'issue', type: 'string', description: 'Customer issue description', required: true },
      { name: 'customerTier', type: 'string', description: 'Customer tier (free/pro/enterprise)', required: true },
    ],
  })
  async triageIssue(input: { issue: string; customerTier: string }) {
    // Render the prompt from the registry
    const tpl = PromptRegistry.get<{ issue: string; customerTier: string }>('support:ticket:triage');
    const prompt = tpl.render(input);

    // Pass the rendered prompt to your LLM
    const response = await callLLM(prompt);
    return JSON.parse(response);
  }
}

// ─── 4. Expose as MCP server ──────────────────────────────────────────────────

const registry = new ToolRegistry();
registry.registerAgentTools('support', new SupportAgent());

const server = createMcpServer({ registry });
server.listenStdio();

Best Practices

Namespace All Keys

Always use the package:scope:action convention. This prevents accidental collisions between your prompts and built-in library prompts.

Prefer register() for Defaults, override() for Customisation

  • Use register() in your shared libraries — it is a no-op if the key already exists, so it won't clobber user overrides set before the import.
  • Use override() in application entry points to swap built-in prompts.

Version Every Template

Always provide a version string in metadata. This enables get(key, version) for rollbacks, A/B testing, and audit trails.

Load Store on Startup, Save on Change

// Startup
await PromptRegistry.loadAll();

// When an admin updates a prompt via your API
PromptRegistry.override(key, newTemplate);
await PromptRegistry.save(key);

Keep Templates Focused

One template per task. Short, focused templates are easier to version and test than large multi-purpose ones.

  • Agent Package — Uses @hazeljs/prompts for all internal system prompts; override them to tune behaviour
  • RAG Package — Uses @hazeljs/prompts for GraphRAG extraction and synthesis prompts
  • MCP Package — Expose prompt-powered tools via the Model Context Protocol
  • AI Package — LLM providers used to execute rendered prompts

Recipes

Recipe: Versioned Prompt with Variables

// File: src/prompts/prompts.service.ts
import { Service } from '@hazeljs/core';
import { PromptRegistry } from '@hazeljs/prompts';

@Service()
export class PromptService {
  constructor(private readonly prompts: PromptRegistry) {}

  async onModuleInit() {
    this.prompts.register({
      name: 'customer-support',
      version: '1.0',
      template: `You are a {{company}} support agent.
The customer's name is {{name}} and their plan is {{plan}}.
Help them with their issue. Be concise and professional.`,
    });
  }

  async getSupportPrompt(name: string, plan: string) {
    return this.prompts.render('customer-support', {
      company: 'Acme Corp',
      name,
      plan,
    });
  }
}

Recipe: A/B Test Prompts

// File: src/prompts/ab-test.service.ts
import { Service } from '@hazeljs/core';
import { PromptRegistry } from '@hazeljs/prompts';

@Service()
export class ABTestService {
  constructor(private readonly prompts: PromptRegistry) {}

  async onModuleInit() {
    this.prompts.register({
      name: 'summary',
      version: '1.0',
      template: 'Summarize the following text in 3 bullet points:\n\n{{text}}',
    });
    this.prompts.register({
      name: 'summary',
      version: '2.0',
      template: 'Write a one-paragraph summary of the following text. Be concise:\n\n{{text}}',
    });
  }

  async getPrompt(text: string, version: string) {
    return this.prompts.render('summary', { text }, { version });
  }
}

For the full source and changelog, see the Prompts package on GitHub.