@x12i/rendrix
v4.3.0
Published
Inline variant template rendering with Handlebars
Maintainers
Readme
Rendrix - Inline Variant Template Rendering
A powerful TypeScript library for rendering Handlebars templates with multiple memory contexts and priority-based token resolution. This system allows you to manage complex template rendering scenarios with separate memory objects that automatically resolve tokens based on priority.
Features
- 🎯 Multi-Memory System: Four distinct memory types with priority-based resolution
- 🔧 Custom Handlebars Helpers: Built-in helpers for common operations
- ✅ Template Verification: Detect unresolved template variables
- 📊 Template Resolution Reporting: See which tokens were parsed vs unparsed (template+memory) and detect leftover
{{}}in output - 📝 TypeScript Support: Full type definitions included
- 🚀 JSON Template Enrichment: Automatically enrich JSON templates based on configuration
- 🔀 Priority-Based Resolution: Tokens resolve automatically regardless of which memory object they're in
- 🧩 Partial Rendering (v3.5): Staged template rendering with incremental state accumulation
- 💬 Chat Message Support (v3.5): Structured chat message rendering using
{{#message}}helper - 📚 Few-Shot Examples (v3.5): Flexible management and formatting of prompt examples
- 🔗 Pipeline Composition (v3.5): Modular prompt building using template sections
- 🏗️ Structured Output (v3.5): JSON schema-based format instructions and response parsing
- 📁 File content inlining:
{{file:<filename>}}replaced by provided file contents (filesContent) - ⚠️ Template protocol (v4): MUST vs optional
{{path | fallback}}, structuredTemplateResolutionError, opt-insubPathSearchwith JSON default root order — same rules forrenderandrenderChat - 🧠 Semantic core directives:
{{core:analysis}},{{core:decision}},{{core:question}}are directive tokens (not memory paths)
Installation
npm install @x12i/rendrixQuick Start
import { render } from '@x12i/rendrix';
const template = `
# Project: {{organization.name}}
{{#if (contains flags "debug")}}
⚠️ Debug mode enabled
{{/if}}
Job Objective: {{job.objective}}
Current Task: {{task.objective}}
{{#if productInfo}}
# Product Information
{{productInfo}}
{{/if}}
`;
const workingMemory = {
flags: ['debug', 'verbose'],
organization: {
name: 'My Company',
industry: 'Technology'
},
job: {
objective: 'Build amazing software'
},
task: {
objective: 'Implement feature X'
},
productInfo: {
name: 'My Product',
features: ['Feature 1', 'Feature 2'],
price: '$99'
}
};
const output = render(template, workingMemory);
console.log(output);Core Concepts
Memory system and effective root (merged context)
Rendering uses one merged context object: the four memory arguments are layers combined in a fixed order. Templates should not be thought of as “reading from one of four separate roots”; they read paths on the merged result (e.g. {{input.question}}, {{execution.answeredQuestions}}) wherever those keys exist after the overlay.
Who merges what
- ai-gateway (or your caller) builds
workingMemory,experienceMemory,knowledgeMemory, andshortTermMemory, and should merge per-requesttemplateTokensinto short-term before callingrender/renderChat. - This library merges the four objects into the effective root inside
buildTemplateRootContext(same order asrender).
Strict precedence on top-level key clash (rank 1 = wins). The parser exposes four memory arguments; ranks 1–2 both live in shortTermMemory after the gateway assembles it — merge gateway templateTokens into short-term last so per-request keys override other short-term fields.
| Rank | Source | Role |
|------|--------|------|
| 1 | Gateway templateTokens (merged into shortTermMemory) | Per-request overrides; strongest |
| 2 | Rest of shortTermMemory | Session / turn data |
| 3 | workingMemory | Primary job / task / user payload |
| 4 | experienceMemory | Enriched / learned context |
| 5 | knowledgeMemory | Reference / RAG-style context |
Implementation detail: the parser merge applies knowledge → experience → working → short-term (each spread overwrites duplicate top-level keys from the previous). So shortTermMemory wins over workingMemory, and within STM the gateway controls which keys win via its own merge order.
The MEMORY_PRIORITY export still uses higher number = stronger (shortTermMemory: 4 … knowledgeMemory: 1) for any code that keys off that map.
Layer contents (reference)
- ShortTermMemory — Flexible key-value storage; session- or turn-specific data; gateway should fold
templateTokenshere before render. - WorkingMemory — Runtime state:
job,task,entity,organization, execution-shaped data, etc. - ExperienceMemory — Learned context: knowHows, narratives, experiences, insights, …
- KnowledgeMemory — General knowledge base; weakest overlay.
Priority-Based Token Resolution
Top-level keys from any layer appear on the merged root; if the same key exists in multiple layers, precedence follows the table above. You do not need to nest everything under workingMemory.* in templates unless you want the namespaced copy (see below).
// Same top-level key in multiple memories → strongest layer wins
const shortTermMemory = { name: 'Short Term' };
const workingMemory = { job: {}, task: {}, name: 'Working' };
// Template: {{name}}
// Result: "Short Term"Namespaced copies: The merged root also exposes workingMemory, shortTermMemory, experienceMemory, and knowledgeMemory as nested objects for templates that need them (e.g. {{workingMemory.job.objective}}).
Automatic Markdown Conversion
Objects and arrays are automatically converted to markdown by default! This means you can use {{variableName}} directly and get beautifully formatted markdown output with title-case keys.
Example:
const workingMemory = {
productInfo: {
name: 'CloudSync Pro',
features: ['Storage', 'Collaboration'],
pricing: '$15/month'
}
};
// Template: {{productInfo}}
// Output:
// # Name
// CloudSync Pro
//
// # Features
// - Storage
// - Collaboration
//
// # Pricing
// $15/monthIf you need JSON output instead, use the {{json variableName}} helper:
{{json productInfo}}This will output:
{
"name": "CloudSync Pro",
"features": ["Storage", "Collaboration"],
"pricing": "$15/month"
}API Reference
Template protocol (v4)
Applies to render, renderChat, and (for path resolution) analyzeTemplateResolution when you pass options.
- Simple variable mustaches matching
{{identifier}}or{{a.b.c}}(and optional| fallback) are handled by the protocol. Helpers, blocks,{{file:...}}, and{{json ...}}are unchanged. - Semantic directives —
{{core:value}}:coreis treated as a directive namespace, not object traversal. These tokens render as empty string by default and never trigger missing path errors. - Directive validation:
{{core:}}is invalid and throws. Whitespace is trimmed (for example{{core: analysis}}is parsed as valueanalysis). - MUST —
{{path}}: If the value is missing (undefinedafter lookup, including sub-path search), rendering throwsTemplateResolutionErrorwithtype,token,attemptedPath,availableKeys, and optionaltemplate(fromtemplateId). Valuesnull,"",0,falsecount as present (no throw). - OPTIONAL —
{{path |}}or{{path | fallback text}}: If missing, output is empty or the fallback text; never throws for that token. - Legacy behavior: Pass
renderOptions.silentMissingMustTokens: trueto treat missing MUST tokens as empty strings (Handlebars-style), with no throw. - Default root order when
subPathSearchis enabled and you omitroots: seesrc/config/default-template-protocol.json(copied todist/config/on build). Edit that JSON to change priority; it does not enable sub-path search by itself.
Sub-path search (subPathSearch)
Default: subPathSearch.enabled is off unless you set it. With search off, resolution is full path only (strict): missing MUST paths throw TemplateResolutionError after the merge.
Opt-in on TemplateRenderOptions / ChatRenderOptions:
subPathSearch?: {
enabled?: boolean; // default false — must be true to search
roots?: string[]; // omit → use parser package defaults (do not duplicate in gateway)
};When enabled: true and roots is omitted, defaults come only from DEFAULT_TEMPLATE_PROTOCOL_CONFIG / src/config/default-template-protocol.json (copied to dist/config/ on build). Recommended default roots (skills / graphs; order matters): execution, then input, then inputs — so execution-shaped data is tried before generic input buckets.
When roots is provided, that ordered list is used; first successful match wins along the algorithm in resolveTokenPath (template-resolution.ts): exact path first, then leading-root swap, then prepend each root.
render(template, workingMemory, shortTermMemory?, experienceMemory?, knowledgeMemory?, functionsMap?, choiceOptions?, filesContent?, renderOptions?)
Renders a Handlebars template with all memory contexts. Uses a fresh Handlebars.create() instance per call (isolated helpers). Tokens are resolved with priority-based lookup. Objects and arrays are automatically converted to markdown format. Any {{file:<filename>}} token is replaced by the corresponding entry in filesContent when entry.file matches <filename>.
Parameters:
template: string- The Handlebars template stringworkingMemory: WorkingMemory- Runtime state object (required)shortTermMemory?: ShortTermMemory- Optional short-term memory (highest priority)experienceMemory?: ExperienceMemory- Optional experience memoryknowledgeMemory?: KnowledgeMemory- Optional knowledge memory (lowest priority)functionsMap?: FunctionMap- Optional function map for function calls in templateschoiceOptions?: ChoiceOptions- Optional choice options for options helperfilesContent?: FilesContent- Optional list of{ file, content };{{file:<filename>}}is replaced bycontentwhenfilematchesrenderOptions?: TemplateRenderOptions- OptionaltemplateId,subPathSearch,silentMissingMustTokens(see Template protocol (v4))
Returns: string - Rendered template
Smart Input token
Rendrix supports a special case-insensitive insertion token:
{{smartInput}}(also{{ smartinput }},{{SMARTINPUT}}, etc.)
When this exact token appears in a template, Rendrix replaces the whole token (before the normal Handlebars render) with markdown generated from runtime configuration supplied via renderOptions.smartInput / ChatRenderOptions.smartInput.
Important boundaries
- Only the exact token
{{smartInput}}(case-insensitive, whitespace allowed) is special. - Rendrix does not treat these as the Smart Input macro:
{{smartInput.paths}}{{#if smartInput}} ... {{/if}}{{json smartInput}}
Behavior
- If the template contains
{{smartInput}}andrenderOptions.smartInputis omitted, it is replaced with empty string. - If
smartInput.pathsis empty,{{smartInput}}renders as empty string. - Paths are resolved against the same merged render root that normal tokens use (
buildTemplateRootContextoutput). - After generation, the markdown is also injected into the render root as
smartInputMarkdown, so templates can use{{smartInputMarkdown}}as a normal variable path token (case-sensitive).
Example
import { render } from '@x12i/rendrix';
const out = render(
"Context:\\n{{smartInput}}\\nQuestion: {{input.question}}",
{
job: {},
task: {},
input: { question: "What exposure exists?" },
executionMemory: {
graphOutputs: {
candidateIngressPaths: [{ pathId: "p1" }]
}
},
xynthesized: {
job: {
topologyReachabilitySummary: { mainIngress: "shared-alb" }
}
}
} as any,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
{
smartInput: {
paths: [
{
title: "Topology summary",
path: "xynthesized.job.topologyReachabilitySummary"
},
{
title: "Candidate ingress paths",
path: "executionMemory.graphOutputs.candidateIngressPaths",
required: true
}
]
}
}
);enrichJsonTemplate scope (phase 1)
The Smart Input token is supported in render / renderChat first. It is not supported in enrichJsonTemplate in phase 1.
Example:
import { render, FunctionMap } from '@x12i/rendrix';
// Without function map
const output = render(
'Hello {{name}}!',
{ job: {}, task: {} }, // workingMemory (required)
{ name: 'World' }, // shortTermMemory (optional)
{}, // experienceMemory (optional)
{} // knowledgeMemory (optional)
);
// With function map
type MyFunctions = {
format: (value: any) => string;
};
const functionsMap: MyFunctions = {
format: (value) => JSON.stringify(value, null, 2)
};
const output2 = render(
'Formatted: {{function "format" data}}',
{ job: {}, task: {}, data: { key: 'value' } },
undefined,
undefined,
undefined,
functionsMap
);
// With filesContent: {{file:<filename>}} is replaced by the matching entry's content
const output3 = render(
'Instructions:\n{{file:readme}}\n---\nContext: {{name}}',
{ job: {}, task: {} },
{ name: 'Alice' },
undefined,
undefined,
undefined,
undefined,
[{ file: 'readme', content: 'Follow these steps.' }]
);
// "Instructions:\nFollow these steps.\n---\nContext: Alice"
// Optional: legacy silent missing MUST tokens, or template id for errors
render('Hello {{maybe}}!', { job: {}, task: {} }, undefined, undefined, undefined, undefined, undefined, undefined, {
silentMissingMustTokens: true,
});
render('{{input.x}}', wm, undefined, undefined, undefined, undefined, undefined, undefined, {
subPathSearch: { enabled: true },
templateId: 'my-prompt',
});See also: File Content Inlining for {{file:<filename>}}, filesContent, and usage in render, partial, pipeline, enrichJsonTemplate, renderChat, and analyzeTemplateResolution.
enrichJsonTemplate(jsonTemplate, workingMemory?, shortTermMemory?, experienceMemory?, knowledgeMemory?, opts?)
Enriches a JSON template string by rendering Handlebars conditionals. Supports optional JSON validation and JSONC (JSON with comments) parsing.
Parameters:
jsonTemplate: string- JSON template string with Handlebars syntaxworkingMemory?: WorkingMemory- Optional working memory contextshortTermMemory?: ShortTermMemory- Optional short-term memoryexperienceMemory?: ExperienceMemory- Optional experience memoryknowledgeMemory?: KnowledgeMemory- Optional knowledge memoryopts?: EnrichJsonTemplateOptions- Optional validation and parsing options; may includetemplateRenderOptions(same shape asTemplateRenderOptionsforrender) to passtemplateId,subPathSearch, orsilentMissingMustTokens
Returns: string - Enriched JSON string with conditionals resolved
Example (Basic):
import { enrichJsonTemplate } from '@x12i/rendrix';
const jsonTemplate = `{
"confidence": 0.85{{#if includeCitations}},
"citations": ["Citation 1"]
{{/if}}{{#if includeReasoning}},
"reasoning": "Explanation"
{{/if}}
}`;
const workingMemory = {
job: {},
task: {},
includeCitations: true,
includeReasoning: true
};
const enriched = enrichJsonTemplate(jsonTemplate, workingMemory);
// Returns JSON with citations and reasoning includedExample (With Validation):
import { enrichJsonTemplate } from '@x12i/rendrix';
import type { JSONSchemaType } from 'ajv';
const schema: JSONSchemaType<{ confidence: number; citations?: string[] }> = {
type: 'object',
properties: {
confidence: { type: 'number' },
citations: { type: 'array', items: { type: 'string' } }
},
required: ['confidence'],
additionalProperties: false
};
const enriched = enrichJsonTemplate(
jsonTemplate,
workingMemory,
undefined,
undefined,
undefined,
{
validate: true,
schema,
parseMode: 'strict', // or 'jsonc' for JSON with comments
failOnUnresolvedTokens: true
}
);See also: JSON Enrichment Improvements for detailed documentation on validation and JSONC support.
verifyTemplate(output)
Checks the rendered output for any {{...}} still present. Useful to catch leftover mustaches. Note: Simple MUST variables that are missing either throw during render (default) or render as empty if you set silentMissingMustTokens: true — in the latter case, verifyTemplate will not see them. Non-rewritten helpers or typos may still leave {{ in the output. Use analyzeTemplateResolution to find which paths are missing in memory before rendering.
Parameters:
output: string- The rendered template output
Returns: { valid: boolean, unresolvedTokens: string[] }
analyzeTemplateResolution(template, workingMemory, shortTermMemory?, experienceMemory?, knowledgeMemory?, filesContent?, resolutionOptions?)
Analyzes template + memory and reports which variable paths were parsed (resolved, value ≠ undefined) and which were unparsed (missing in the merged context). Uses the same merge order and path resolution as render() (including subPathSearch when passed in resolutionOptions). Optional tokens {{path | fallback}} count as parsed with the fallback value when the path is missing. Supports filesContent so {{file:<filename>}} is resolved when provided.
Parameters: Same memory/filesContent order as render() (without functionsMap, choiceOptions, or the ninth render argument). Optional last argument resolutionOptions?: TemplateRenderOptions (typically only subPathSearch is used here).
Returns: { parsed: Array<{ path: string; value: unknown }>; unparsed: string[] }
Example:
import { analyzeTemplateResolution } from '@x12i/rendrix';
const r = analyzeTemplateResolution(
'Hello {{name}}! Project: {{project}}.',
{ job: {}, task: {} },
{ name: 'Alice' }
);
// r.parsed = [ { path: 'name', value: 'Alice' } ]
// r.unparsed = [ 'project' ]See also: Template Resolution Reporting for analyzeTemplateResolution, verifyTemplate, and when to use each.
listTokens(template, options?)
Returns token references from template source without rendering. This is useful for contract discovery (for example, required semantic paths) before runtime value resolution.
Compatibility: additive API; does not change render() or renderChat() behavior.
Type shape:
type TemplateTokenKind = 'path' | 'helper' | 'block' | 'core' | 'unknown';
interface TemplateTokenRef {
raw: string; // token content without outer {{ }}
path?: string;
directive?: string;
value?: string;
kind: TemplateTokenKind;
start: number;
end: number;
}Options:
includeDuplicates?: boolean(defaultfalse) — include repeated references of the same token expression.
Example:
import { listTokens } from '@x12i/rendrix';
const refs = listTokens('{{core:analysis}} {{#if x}}{{/if}} {{json payload}} {{metadata.intent}}');
// refs includes:
// - { kind: 'core', directive: 'core', value: 'analysis', ... }
// - { kind: 'block', raw: '#if x', ... }
// - { kind: 'helper', raw: 'json payload', ... }
// - { kind: 'path', path: 'metadata.intent', ... }resolveTokenValues({ template, context, selectors, options? })
Resolves only selected path tokens from a template and context, without full template rendering. Helpers/blocks/directive tokens are ignored for value resolution.
Compatibility: additive API; does not change existing template rendering behavior.
Selectors:
- exact path:
question,metadata.intent - segment wildcard:
metadata.*(matches one segment, e.g.metadata.intent)
Missing handling:
- default (
strict: false): returns entries withfound: false - strict (
strict: true): throws if any selected token is missing or a selector matches no path token
Example:
import { resolveTokenValues } from '@x12i/rendrix';
const values = resolveTokenValues({
template: '{{core:analysis}} {{metadata.intent}} {{question}}',
context: {
metadata: { intent: 'summarize' },
},
selectors: ['metadata.intent', 'question'],
});
// [
// { token: 'metadata.intent', selector: 'metadata.intent', value: 'summarize', found: true },
// { token: 'question', selector: 'question', value: undefined, found: false }
// ]listDirectiveValues(template, directive)
Returns directive values in source order (including duplicates).
Example:
import { listDirectiveValues } from '@x12i/rendrix';
const values = listDirectiveValues(
'System:\n- {{core:analysis}}\n- {{core: decision}}\n- {{core:analysis}}',
'core'
);
// ['analysis', 'decision', 'analysis']renderChat(template, workingMemory, shortTermMemory?, experienceMemory?, knowledgeMemory?, options?)
Returns an array of ChatMessage objects from a template that uses {{#message}}. Same template protocol as render (MUST / optional |, subPathSearch, TemplateResolutionError, silentMissingMustTokens). There is no functionsMap or choiceOptions on this entry point; pass filesContent inside options when needed.
options?: ChatRenderOptions extends TemplateRenderOptions with stripEmpty, trimContent, mergeConsecutive, and filesContent.
registerHelpers(handlebars?)
Registers custom Handlebars helpers on the given instance (default: the global Handlebars). render() and renderChat() use Handlebars.create() and register helpers on that instance automatically; you usually do not need to call this unless you compile templates yourself. When you pass functionsMap to render, function helpers are registered on the same per-call instance.
Exports (protocol-related)
TemplateResolutionError— thrown when a MUST token cannot be resolvedDEFAULT_TEMPLATE_PROTOCOL_CONFIG— parsed default JSON (includingsubPathSearch.roots)buildTemplateRootContext,wrapForTemplate— same context shape asrender(e.g. for advanced use)registerChatHelpersOn(handlebars)— register{{#message}}on a specific Handlebars instance
Extended Features (v3.5+)
Rendrix v3.5 introduced staged rendering, chat, and pipeline features; v4 aligns renderChat with the same template protocol as render (MUST / optional |, subPathSearch, errors).
Partial Rendering
Partial rendering allows you to accumulate state incrementally before the final render.
import { partial } from '@x12i/rendrix';
const p = partial('Hello {{name}}! Welcome to {{place}}.', { name: 'Alice' });
const p2 = p.partial(undefined, { place: 'Wonderland' });
const result = p2.render(); // "Hello Alice! Welcome to Wonderland."Chat Message Rendering
Render structured chat messages using the {{#message}} helper. renderChat uses the same preprocessing and variable rules as render (including TemplateResolutionError for missing MUST paths, {{path | fallback}}, subPathSearch, and silentMissingMustTokens on ChatRenderOptions).
{{#message role="system"}}
You are a {{role}} assistant.
{{/message}}
{{#message role="user"}}
My name is {{userName}}.
{{/message}}import { renderChat } from '@x12i/rendrix';
const messages = renderChat(template, { job: {}, task: {}, role: 'helpful' }, { userName: 'Bob' });
// Returns: [ { role: 'system', content: '...' }, { role: 'user', content: '...' } ]
// Same optional protocol flags as render (on the last options argument):
// renderChat(template, workingMemory, stm, em, km, { silentMissingMustTokens: true, subPathSearch: { enabled: true } })Few-Shot Examples
Manage prompt examples with flexible selection strategies.
import { fewShot } from '@x12i/rendrix';
const fs = fewShot({
examples: [
{ input: 'Hi', output: 'Hello' },
{ input: 'Bye', output: 'Goodbye' }
],
maxExamples: 1,
selector: 'random'
});
const prompt = fs.format({}, (ex) => `Input: ${ex.input}\nOutput: ${ex.output}`);Pipeline Composition
Build modular prompts from reusable template sections.
import { pipeline } from '@x12i/rendrix';
const pipe = pipeline([
{ name: 'header', template: '# {{title}}' },
{ name: 'body', template: 'Content: {{content}}' },
{ name: 'footer', template: 'Footer: {{footer}}', condition: 'showFooter' }
]);
const output = pipe.render({ title: 'My Doc' }, { content: '...', showFooter: true });Structured Output Parsers
Generate format instructions and parse JSON responses.
import { fromSchemas } from '@x12i/rendrix';
const parser = fromSchemas([
{ name: 'answer', description: 'The final answer', type: 'string' }
]);
console.log(parser.getFormatInstructions());
const data = parser.parse(llmResponse);Custom Helpers
The library provides several custom Handlebars helpers:
| Helper | Usage | Description |
|--------|-------|-------------|
| eq | {{#if (eq value "test")}} | Equality check |
| neq | {{#if (neq status "ready")}} | Inequality check |
| gt | {{#if (gt value 10)}} | Greater than |
| lt | {{#if (lt value 5)}} | Less than |
| contains | {{#if (contains flags "debug")}} | Array contains |
| and | {{#if (and flag1 flag2)}} | Logical AND |
| or | {{#if (or flag1 flag2)}} | Logical OR |
| json | {{json variableName}} | Convert object/array to JSON format (default is markdown) |
| function | {{function "name" expression}} or {{function "name"}} | Call a function from functionsMap (optional). Can be used in conditionals: {{#if (function "check" value)}} |
| limit | {{limit <number> <value>}} | Limit the number of words in text/string |
| limitProperties | {{limitProperties <number> <value>}} | Limit words in each property of an object (use namespaced access for objects) |
| limitArray | {{limitArray <number> <value>}} | Limit the number of items in an array (use namespaced access for arrays) |
| smartLimit | {{smartLimit "<items>/<words>" <value>}} | Apply all limit strategies: limit items and words (use namespaced access for objects/arrays) |
| section | {{section "title"}} | Create a formatted section header with clear separator |
| ifSection | {{#ifSection <condition> "title"}} ... {{/ifSection}} | Conditional section that combines if logic with section formatting |
| options | {{options}} | Render choice options list (requires choiceOptions parameter) |
| getOption | {{getOption <selection>}} | Get selected option value (requires choiceOptions parameter) |
Function Map
The Function Map feature allows you to register custom functions that can be called from templates. This is optional - you only need to provide a functionsMap if you want to use function calls in your templates.
What is a Function Map?
A Function Map is a typed object that maps string keys to functions, where each key is bound to an exact function signature.
In TypeScript, it is the safest way to register and call functions by name without losing type safety.
Why use a Function Map?
A Function Map gives you:
- ✅ Compile-time validation of function names
- ✅ Compile-time validation of arguments and return types
- ✅ No mismatch between a function's name and its implementation
- ✅ Simple runtime behavior (just plain JavaScript)
This pattern is ideal for tool registries, skill systems, routers, and dispatchers.
Basic Example
import { render, FunctionMap } from '@x12i/rendrix';
// Define your function map type
type MyFunctionMap = {
sum: (a: number, b: number) => number;
greet: (name: string) => string;
format: (value: any) => string;
getTimestamp: () => string;
hasData: (obj: any) => boolean;
isValid: (value: any) => boolean;
};
// Register functions
const functionsMap: MyFunctionMap = {
sum: (a, b) => a + b,
greet: (name) => `Hello ${name}`,
format: (value) => JSON.stringify(value, null, 2),
getTimestamp: () => new Date().toISOString(),
hasData: (obj) => obj && Object.keys(obj).length > 0,
isValid: (value) => value !== null && value !== undefined
};
const template = `
Sum: {{function "sum" 5 3}}
Greeting: {{function "greet" userName}}
Formatted: {{function "format" workingMemory.data}}
Timestamp: {{function "getTimestamp"}}
{{#if (function "hasData" data)}}
Data is available: {{data}}
{{/if}}
{{#if (function "isValid" user)}}
User is valid
{{/if}}
`;
const workingMemory = {
job: {},
task: {},
userName: 'World',
data: { key: 'value' }
};
const output = render(template, workingMemory, undefined, undefined, undefined, functionsMap);Function Call Syntax
Use the function helper to call functions from your functionsMap:
Syntax: {{function "functionName" expression}} or {{function "functionName"}}
- First argument is the function name as a string (required)
- Second argument (optional) is the expression value to pass to the function
- If no expression is provided, the function is called with no arguments
- Values are passed AS IS: If the expression is an object, it stays an object (not converted to markdown)
Examples:
<!-- With expression - value is passed AS IS (object stays object, string stays string) -->
{{function "format" productInfo}}
{{function "format" workingMemory.data}} <!-- Use namespaced access for original objects -->
<!-- Without expression - function called with no arguments -->
{{function "getTimestamp"}}
<!-- In conditionals - function should return boolean -->
{{#if (function "hasData" data)}}
Data is available
{{/if}}
{{#if (function "isValid" workingMemory.user)}}
User is valid
{{else}}
User is invalid
{{/if}}
{{#if (function "checkFlag")}}
Flag is enabled
{{/if}}Important Notes
- Values are passed AS IS: When you use
{{function "name" expression}}, the value from the expression is passed directly to the function. If it's an object, it remains an object (not converted to markdown). - Function Map is optional: You only need to provide
functionsMapif you're using function calls in your templates. - Type safety: TypeScript will validate function names and signatures at compile time.
- Error handling: If a function is not found or throws an error, a descriptive error message is thrown.
- Use in conditionals: Functions can be used in
{{#if}}conditionals using subexpressions. The function should return a boolean value:{{#if (function "checkCondition" variable)}} Content here {{/if}}
When to use Function Maps
Use a Function Map when you need:
- Custom formatting or transformation functions
- Dynamic data processing in templates
- Reusable utility functions across templates
- AI tool / skill registries
- Plugin systems
Key Takeaway
A Function Map turns string-based function calls into fully typed, compile-time-safe operations, while staying simple and runtime-light.
Variable Access
Direct Access (Recommended)
Variables can be accessed directly without prefixes, thanks to priority-based resolution. Objects and arrays are automatically converted to markdown format by default:
{{#if execution}}
# Execution
{{execution}}
{{/if}}
{{name}}
{{job.objective}}
{{task.context}}
{{productInfo}} <!-- Automatically converted to markdown -->Note: For conditionals like {{#if execution}}, use the original object from namespaced access (e.g., {{#if workingMemory.execution}}) or the object will be truthy as a string. Direct access in conditionals works because the original objects are preserved in namespaced contexts.
JSON Output
If you need JSON format instead of markdown, use the json helper with namespaced access to get the original object:
{{json workingMemory.productInfo}} <!-- Outputs JSON instead of markdown -->Note: Use namespaced access (e.g., workingMemory.productInfo) with the json helper to access the original object. Direct access like {{json productInfo}} will return the markdown string since objects are automatically converted to markdown in the merged context.
Namespaced Access
You can also access variables with explicit namespaces:
{{#if workingMemory.flags}}
{{workingMemory.job.objective}}
{{/if}}
{{shortTermMemory.userName}}
{{experienceMemory.knowHows}}
{{knowledgeMemory.facts}}Limit Helpers
The library provides several helpers to limit the size of output content, useful for controlling token usage and output length.
limit - Word Limit for Text
Limits the number of words in a text string:
{{limit 10 longText}} <!-- Limits to 10 words -->Example:
const workingMemory = {
job: {},
task: {},
longText: 'This is a very long text that has many words in it'
};
// Template: {{limit 5 longText}}
// Output: "This is a very long..."limitProperties - Word Limit for Object Properties
Limits the number of words in each property of an object. Use namespaced access to get the original object:
{{limitProperties 5 workingMemory.object}} <!-- Limits each property to 5 words -->Example:
const workingMemory = {
job: {},
task: {},
productInfo: {
name: 'Product Name',
description: 'This is a very long description with many words',
price: 'Expensive'
}
};
// Template: {{limitProperties 3 workingMemory.productInfo}}
// Output: Markdown with each property limited to 3 wordslimitArray - Item Limit for Arrays
Limits the number of items in an array. Use namespaced access to get the original array:
{{limitArray 3 workingMemory.array}} <!-- Limits to 3 items -->Example:
const workingMemory = {
job: {},
task: {},
features: ['Feature 1', 'Feature 2', 'Feature 3', 'Feature 4', 'Feature 5']
};
// Template: {{limitArray 2 workingMemory.features}}
// Output: Markdown list with only first 2 itemssmartLimit - Combined Limiting Strategy
Applies multiple limit strategies at once. Takes a spec in the format "<items>/<words>" where:
items- Maximum number of items/propertieswords- Maximum number of words per item/property
{{smartLimit "5/100" workingMemory.data}} <!-- Limit to 5 items, 100 words each -->Example:
const workingMemory = {
job: {},
task: {},
data: {
prop1: 'Long text with many words',
prop2: 'Another long text',
prop3: 'Third property',
prop4: 'Fourth property'
}
};
// Template: {{smartLimit "2/3" workingMemory.data}}
// Output: Only first 2 properties, each limited to 3 wordsHow smartLimit works:
- For arrays: Limits to
itemsnumber of array elements, then limits words in each element towords - For objects: Limits to
itemsnumber of properties, then limits words in each property value towords - For strings: Limits words to
words(items limit is ignored)
Important Notes:
- For
limitProperties,limitArray, andsmartLimitwith objects/arrays, use namespaced access (e.g.,workingMemory.data) to get the original object/array - Direct access will work with strings, but for objects/arrays you need namespaced access to avoid the markdown conversion
- All limit helpers add
...when content is truncated
Section Helpers
The library provides helpers for creating clearly formatted sections in your templates.
section - Section Header
Creates a formatted section header with a clear separator frame:
{{section "Section Title"}}Example:
const workingMemory = {
job: {},
task: {},
productInfo: {
name: 'Product Name',
price: '$99'
}
};
// Template:
// {{section "Product Information"}}
// {{productInfo}}
// Output:
// ══════════════════════════════════════════════════
// Product Information
// ══════════════════════════════════════════════════
//
// # Name
// Product Name
//
// # Price
// $99ifSection - Conditional Section
Combines conditional logic with section formatting. Only displays the section header and content if the condition is true:
{{#ifSection <condition> "Section Title"}}
Content here
{{/ifSection}}You can also use an {{else}} block:
{{#ifSection hasData "Data Section"}}
{{data}}
{{else}}
No data available
{{/ifSection}}Example:
const workingMemory = {
job: {},
task: {},
execution: {
step1: 'Completed',
step2: 'In progress'
},
hasData: true
};
// Template:
// {{#ifSection hasData "Execution Status"}}
// {{execution}}
// {{/ifSection}}
// Output (when hasData is true):
// ══════════════════════════════════════════════════
// Execution Status
// ══════════════════════════════════════════════════
//
// # Step1
// Completed
//
// # Step2
// In progressExample with Arrays:
const workingMemory = {
job: {},
task: {},
execution: {
answeredQuestions: [
{ question: 'Q1', shortAnswer: 'A1', longAnswer: 'L1' },
{ question: 'Q2', shortAnswer: 'A2', longAnswer: 'L2' }
]
}
};
// Template (use namespaced access for arrays):
// {{#ifSection workingMemory.execution.answeredQuestions "Previously Answered Questions"}}
// {{#each workingMemory.execution.answeredQuestions}}
// Q: {{question}}
// Short: {{shortAnswer}}
// Long: {{longAnswer}}
//
// {{/each}}
// {{/ifSection}}
// Output (when array has items):
// ═══════════════════════════════════════════════════════════════
// Previously Answered Questions
// ═══════════════════════════════════════════════════════════════
//
// Q: Q1
// Short: A1
// Long: L1
//
// Q: Q2
// Short: A2
// Long: L2
//Note: When the array is empty, the entire section (header and content) is not rendered.
Use Cases:
- Organize long templates into clear sections
- Conditionally show entire sections based on data availability
- Create visually distinct sections in output documents
- Combine conditional logic with section formatting in one helper
Important Notes:
- Array handling: Empty arrays (
[]) are treated as falsy - the section won't render - Object handling: Empty objects (
{}) are treated as falsy - the section won't render - String handling: Empty strings are treated as falsy
- Use namespaced access for arrays/objects in conditions:
{{#ifSection workingMemory.execution.answeredQuestions "Title"}}to access the original array/object instead of the markdown string - Direct access works for simple values, but for arrays/objects, use namespaced access to ensure proper truthiness checking
Options Helpers
The library provides helpers for rendering and selecting from choice options. These helpers require a choiceOptions parameter to be passed to the render function (similar to functionsMap).
options - Render Choice Options
Renders a formatted list of options from the choiceOptions parameter:
{{options}}Input Format:
type ChoiceOptions = Array<{
title: string;
type: 'index' | 'enums';
options: any[];
}>;Example:
const choiceOptions = [{
title: 'Choose Product',
type: 'index',
options: ['Product A', 'Product B', 'Product C']
}];
// Template: {{options}}
// Output:
// Choose Product:
// 1: Product A
// 2: Product B
// 3: Product CIndexed Options:
- Uses 1-based numbering (1, 2, 3, ...)
- Simple format:
1: Option Value
Enum Options:
- Uses the enum key as the identifier
- Format:
enumKey: Option Value - For objects:
{ 'key': 'value' }→key: value
Nested Options:
- Supports parent-child relationships
- Format:
[parent, [child1, child2, ...]] - Nested items shown with indentation:
- childKey: childValue
getOption - Get Selected Option
Returns the selected option value from choiceOptions:
{{getOption 1}} <!-- Index-based selection (1-based) -->
{{getOption "premium"}} <!-- Enum-based selection -->
{{getOption "1,2"}} <!-- Nested selection (parent,child) -->
{{getOption 0}} <!-- Special: None -->
{{getOption "other"}} <!-- Special: Other -->Supported Selection Types:
- Number (1-based): For indexed options, e.g.,
1,2,3 - String (enum key): For enum options, e.g.,
"premium","basic" - Nested:
"parentKey,childKey"or"1,2"for nested selections - Special values:
0,-1,"none","other"return"None"or"Other"
Example:
const choiceOptions = [{
title: 'Choose Service',
type: 'enums',
options: [
{ 'basic': 'Basic Service - $10/month' },
{ 'premium': 'Premium Service - $25/month' }
]
}];
// Template: {{getOption "premium"}}
// Output: "Premium Service - $25/month"Important Notes:
choiceOptionsmust be passed as the 7th parameter torender()- Index count starts at 1 (not 0)
- For enum options with objects, returns the value (not the object)
- Special values (
0,-1,"none","other") return string labels
Type Definitions
WorkingMemory
interface WorkingMemory {
flags?: string[];
entity?: {
name?: string;
type?: string;
description?: string;
};
organization?: {
name?: string;
industry?: string;
size?: string;
description?: string;
};
job: {
objective?: string | object | object[] | string[];
context?: string | object | object[] | string[];
data?: object | object[] | string[] | string;
narrative?: object | object[] | string[] | string;
bigPicture?: object | object[] | string[] | string;
};
task: {
objective?: string | object | object[] | string[];
context?: string | object | object[] | string[];
data?: object | object[] | string[] | string;
narrative?: object | object[] | string[] | string;
governance?: object | object[] | string[] | string;
};
}Note: job and task are required fields (cannot be undefined).
ShortTermMemory
interface ShortTermMemory {
[key: string]: string | number | boolean | object | object[];
}Flexible key-value storage for temporary data. Highest priority (4).
ExperienceMemory
interface ExperienceMemory {
knowHows?: object | object[] | string[] | string;
narratives?: object | object[] | string[] | string;
experiences?: object | object[] | string[] | string;
insights?: object | object[] | string[] | string;
learnings?: object | object[] | string[] | string;
lessons?: object | object[] | string[] | string;
frameworks?: object | object[] | string[] | string;
tools?: object | object[] | string[] | string;
}Structured experience data. Priority 2.
KnowledgeMemory
interface KnowledgeMemory {
[key: string]: string | number | boolean | object | object[];
}Flexible key-value storage for general knowledge. Lowest priority (1).
Memory Priority Configuration
The priority system is configurable via MEMORY_PRIORITY:
import { MEMORY_PRIORITY } from '@x12i/rendrix';
console.log(MEMORY_PRIORITY);
// {
// shortTermMemory: 4, // Strongest - highest priority
// workingMemory: 3,
// experienceMemory: 2,
// knowledgeMemory: 1 // Weakest - lowest priority
// }Priority Order (Strongest to Weakest):
shortTermMemory(4) - Takes precedence over all othersworkingMemory(3)experienceMemory(2)knowledgeMemory(1) - Gets overwritten by others
Common Patterns
1. Direct Variable Access
{{#if requireCitations}}
- Include citations
{{/if}}
{{#if includeReasoning}}
- Provide reasoning
{{/if}}2. Priority-Based Resolution
// Template: {{userName}}
const shortTermMemory = { userName: 'Alice' }; // Wins (Priority 4)
const workingMemory = { userName: 'Bob' }; // Ignored
const knowledgeMemory = { userName: 'Charlie' }; // Ignored
// Result: "Alice"3. JSON Template Enrichment
{
"confidence": 0.85{{#if requireCitations}},
"citations": ["Citation 1", "Citation 2"]
{{/if}}{{#if includeReasoning}},
"reasoning": "Detailed explanation..."
{{/if}}
}4. Structured Data Access
Organization: {{organization.name}}
Entity: {{entity.name}}
Job: {{job.objective}}
Task: {{task.objective}}Examples
Basic Rendering
import { render } from '@x12i/rendrix';
const template = 'Hello {{name}}!';
const workingMemory = {
job: {},
task: {}
};
const shortTermMemory = { name: 'World' };
const output = render(template, workingMemory, shortTermMemory);
// Output: "Hello World!"JSON Enrichment
import { enrichJsonTemplate } from '@x12i/rendrix';
const jsonTemplate = `{
"question": "{{question}}",
"confidence": 0.85{{#if requireCitations}},
"citations": ["Citation 1"]
{{/if}}
}`;
const workingMemory = {
job: {},
task: {},
requireCitations: true
};
const shortTermMemory = {
question: 'What is AI?'
};
const enriched = enrichJsonTemplate(
jsonTemplate,
workingMemory,
shortTermMemory
);Template Verification and Resolution Reporting
import { render, verifyTemplate, analyzeTemplateResolution } from '@x12i/rendrix';
const template = 'Hello {{name}}!';
const workingMemory = { job: {}, task: {} };
const shortTermMemory = { name: 'World' };
// Which tokens are missing in memory? (template + memory)
const resolution = analyzeTemplateResolution(template, workingMemory, shortTermMemory);
if (resolution.unparsed.length > 0) {
console.warn('Unparsed paths:', resolution.unparsed);
}
const output = render(template, workingMemory, shortTermMemory);
// Any {{...}} left in the output?
const verification = verifyTemplate(output);
if (!verification.valid) {
console.error('Unresolved tokens:', verification.unresolvedTokens);
}Best Practices
- Use direct paths on the merged root: Same logical key from
shortTermMemoryoverridesworkingMemoryand lower layers; put per-request overrides (e.g. gatewaytemplateTokens) in short-term, merged last on the gateway side when needed. - Structured Data: Use
WorkingMemoryfor structured job/task/entity data - Temporary Data: Use
ShortTermMemoryfor session-specific or temporary variables - Experience Data: Use
ExperienceMemoryfor learned knowledge, frameworks, tools - General Knowledge: Use
KnowledgeMemoryfor general knowledge base - Type Safety: Always provide required fields (
jobandtaskinWorkingMemory) - Verify Templates: Check for unresolved tokens in production
Contributing
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
MIT License - see LICENSE file for details.
Integrator documentation (.docs)
- Gateway ↔ parser alignment — merged template root, strict memory priority (including
templateTokensvia short-term),subPathSearchdefaults (execution/input/inputs), and responsibility split with ai-gateway.
Functionality Documentation
For detailed information about how the parser handles object manipulation, context merging, and memory resolution, see:
- Object Manipulation - Comprehensive guide to how the parser manipulates and merges memory objects before rendering
- Template Rendering - Complete documentation of the
render()function, including all parameters (required and optional), processing flow, and usage examples - Template Resolution Reporting -
analyzeTemplateResolution(parsed vs unparsed from template+memory) andverifyTemplate(leftover{{}}in output); when to use each - File Content Inlining -
{{file:<filename>}}tokens and thefilesContentoption inrender,partial,pipeline,enrichJsonTemplate,renderChat, andanalyzeTemplateResolution - JSON Enrichment Improvements - Enhanced JSON validation, JSONC support, and error handling for
enrichJsonTemplate()
Support
For questions, issues, or contributions, please open an issue on the repository.
