Prompts Package

npm downloads

The @hazeljs/prompts package provides a centralized, versioned prompt management system for AI applications. Define typed prompt templates with named variable placeholders, store them in a global registry, and back them with any combination of file, Redis, or database storage — all with a zero-overhead synchronous API for hot paths and an async API for persistence.

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

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.

Related

  • 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

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