npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@x12i/rendrix

v4.3.0

Published

Inline variant template rendering with Handlebars

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}}, structured TemplateResolutionError, opt-in subPathSearch with JSON default root order — same rules for render and renderChat
  • 🧠 Semantic core directives: {{core:analysis}}, {{core:decision}}, {{core:question}} are directive tokens (not memory paths)

Installation

npm install @x12i/rendrix

Quick 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, and shortTermMemory, and should merge per-request templateTokens into short-term before calling render / renderChat.
  • This library merges the four objects into the effective root inside buildTemplateRootContext (same order as render).

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: 4knowledgeMemory: 1) for any code that keys off that map.

Layer contents (reference)

  1. ShortTermMemory — Flexible key-value storage; session- or turn-specific data; gateway should fold templateTokens here before render.
  2. WorkingMemory — Runtime state: job, task, entity, organization, execution-shaped data, etc.
  3. ExperienceMemory — Learned context: knowHows, narratives, experiences, insights, …
  4. 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/month

If 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}}: core is 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 value analysis).
  • MUST — {{path}}: If the value is missing (undefined after lookup, including sub-path search), rendering throws TemplateResolutionError with type, token, attemptedPath, availableKeys, and optional template (from templateId). Values null, "", 0, false count 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: true to treat missing MUST tokens as empty strings (Handlebars-style), with no throw.
  • Default root order when subPathSearch is enabled and you omit roots: see src/config/default-template-protocol.json (copied to dist/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 string
  • workingMemory: WorkingMemory - Runtime state object (required)
  • shortTermMemory?: ShortTermMemory - Optional short-term memory (highest priority)
  • experienceMemory?: ExperienceMemory - Optional experience memory
  • knowledgeMemory?: KnowledgeMemory - Optional knowledge memory (lowest priority)
  • functionsMap?: FunctionMap - Optional function map for function calls in templates
  • choiceOptions?: ChoiceOptions - Optional choice options for options helper
  • filesContent?: FilesContent - Optional list of { file, content }; {{file:<filename>}} is replaced by content when file matches
  • renderOptions?: TemplateRenderOptions - Optional templateId, 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}} and renderOptions.smartInput is omitted, it is replaced with empty string.
  • If smartInput.paths is empty, {{smartInput}} renders as empty string.
  • Paths are resolved against the same merged render root that normal tokens use (buildTemplateRootContext output).
  • 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 syntax
  • workingMemory?: WorkingMemory - Optional working memory context
  • shortTermMemory?: ShortTermMemory - Optional short-term memory
  • experienceMemory?: ExperienceMemory - Optional experience memory
  • knowledgeMemory?: KnowledgeMemory - Optional knowledge memory
  • opts?: EnrichJsonTemplateOptions - Optional validation and parsing options; may include templateRenderOptions (same shape as TemplateRenderOptions for render) to pass templateId, subPathSearch, or silentMissingMustTokens

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 included

Example (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 (default false) — 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 with found: 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 resolved
  • DEFAULT_TEMPLATE_PROTOCOL_CONFIG — parsed default JSON (including subPathSearch.roots)
  • buildTemplateRootContext, wrapForTemplate — same context shape as render (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 functionsMap if 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 words

limitArray - 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 items

smartLimit - Combined Limiting Strategy

Applies multiple limit strategies at once. Takes a spec in the format "<items>/<words>" where:

  • items - Maximum number of items/properties
  • words - 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 words

How smartLimit works:

  • For arrays: Limits to items number of array elements, then limits words in each element to words
  • For objects: Limits to items number of properties, then limits words in each property value to words
  • For strings: Limits words to words (items limit is ignored)

Important Notes:

  • For limitProperties, limitArray, and smartLimit with 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
// $99

ifSection - 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 progress

Example 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 C

Indexed 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:

  • choiceOptions must be passed as the 7th parameter to render()
  • 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):

  1. shortTermMemory (4) - Takes precedence over all others
  2. workingMemory (3)
  3. experienceMemory (2)
  4. 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

  1. Use direct paths on the merged root: Same logical key from shortTermMemory overrides workingMemory and lower layers; put per-request overrides (e.g. gateway templateTokens) in short-term, merged last on the gateway side when needed.
  2. Structured Data: Use WorkingMemory for structured job/task/entity data
  3. Temporary Data: Use ShortTermMemory for session-specific or temporary variables
  4. Experience Data: Use ExperienceMemory for learned knowledge, frameworks, tools
  5. General Knowledge: Use KnowledgeMemory for general knowledge base
  6. Type Safety: Always provide required fields (job and task in WorkingMemory)
  7. Verify Templates: Check for unresolved tokens in production

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. 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 templateTokens via short-term), subPathSearch defaults (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) and verifyTemplate (leftover {{}} in output); when to use each
  • File Content Inlining - {{file:<filename>}} tokens and the filesContent option in render, partial, pipeline, enrichJsonTemplate, renderChat, and analyzeTemplateResolution
  • 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.