Prompts Package
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:
| Problem | Without @hazeljs/prompts | With @hazeljs/prompts |
|---|---|---|
| Scattered prompt strings | Prompts live inline with business logic | Centralized in a typed registry |
| No typing | string everywhere | PromptTemplate<{ var1, var2 }> |
| No versioning | Edit in place, lose history | Versioned entries, get(key, version) |
| Hard to override | Modify source code | PromptRegistry.override(key, template) at startup |
| No persistence | Lost on restart if loaded from DB | File / Redis / Database backends |
| Re-deploy to update prompts | Code change required | Hot-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
PromptTemplate<TVariables>— A typed template with{variableName}placeholders. The type parameter enforces which variables must be supplied to.render().PromptRegistry— A global static store. Prompts are registered at import time and read synchronously. Optional store backends add persistence and versioning.- Store Backends — Five implementations with a uniform
PromptStoreinterface: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/promptsfor all internal system prompts; override them to tune behaviour - RAG Package — Uses
@hazeljs/promptsfor 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.