Clay supports three template engines, selectable per generator step via the optional engine field.
All engines have access to Clay's 47+ built-in helpers for string manipulation, logic, and iteration.
| Engine | Best For | Syntax |
|---|---|---|
| Handlebars (default) | Simple substitution, iteration, conditionals | {{pascalCase name}} |
| EJS | Templates needing inline JavaScript logic | <%= helpers.pascalCase(name) %> |
| TypeScript | Complex generation: cross-references, unique imports, graph traversal | CodeGenerator class |
Use the engine field on generate steps to select the engine. The generated file is named after the template's own filename (rendered as a Handlebars template); target is an optional output subdirectory, not the filename. Name the template with the final extension and the per-entity name — don't add .hbs/.ejs (it is stripped from the output name):
{ "generate": "{{pascalCase name}}.ts", "select": "$.model.types[*]" }
{ "generate": "{{pascalCase name}}Service.ts", "select": "$.model.types[*]", "engine": "ejs" }
{ "generate": "index.ts", "select": "$.model", "target": "routes/", "engine": "ts" }
Templates use double curly braces to output values:
// {{name}} Model
class {{pascalCase name}} {
constructor() {
this.id = null;
}
}
Clay automatically adds special context variables to help you navigate your model:
Access the complete root model from anywhere in your template:
<!-- Access model name from anywhere -->
<h1>{{clay_model.name}}</h1>
<!-- Count total types -->
<p>Total types: {{clay_model.model.types.length}}</p>
Reference the parent element in the JSON structure:
<p>Parent name: {{clay_parent.name}}</p>
<p>Parent path: {{clay_parent.json_path}}</p>
The JSON property name of the current element:
<p>Current property: {{clay_key}}</p>
Clay provides extensive string manipulation helpers:
| Helper | Example | Output |
|---|---|---|
camelCase |
{{camelCase "user name"}} |
userName |
pascalCase |
{{pascalCase "user name"}} |
UserName |
kebabCase |
{{kebabCase "userName"}} |
user-name |
snakeCase |
{{snakeCase "userName"}} |
user_name |
upperCase |
{{upperCase "hello"}} |
HELLO |
lowerCase |
{{lowerCase "HELLO"}} |
hello |
capitalize |
{{capitalize "hello world"}} |
Hello world |
startCase |
{{startCase "hello_world"}} |
Hello World |
| Helper | Example | Output |
|---|---|---|
pluralize |
{{pluralize "category"}} |
categories |
singularize |
{{singularize "users"}} |
user |
| Helper | Description | Example |
|---|---|---|
pad |
Pads string to length with spaces | {{pad "abc" 5}} → " abc " |
repeat |
Repeats string N times | {{repeat "*" 3}} → *** |
replace |
Replaces part of string | {{replace "hi" "i" "o"}} → ho |
truncate |
Truncates string to length | {{truncate "hello world" 8}} → hello... |
words |
Splits string into word array | {{#each (words "foo bar")}}{{this}}{{/each}} |
split |
Splits string by delimiter | {{#each (split "a,b,c" ",")}}{{this}}{{/each}} |
Compare values in conditional expressions:
| Helper | Description | Example |
|---|---|---|
eq |
Equals (===) | {{#if (eq status "active")}}Active{{/if}} |
ne |
Not equals (!==) | {{#if (ne count 0)}}Has items{{/if}} |
lt |
Less than (<) | {{#if (lt age 18)}}Minor{{/if}} |
gt |
Greater than (>) | {{#if (gt score 100)}}High{{/if}} |
lte |
Less than or equal (<=) | {{#if (lte count 10)}}Small{{/if}} |
gte |
Greater than or equal (>=) | {{#if (gte age 18)}}Adult{{/if}} |
{{#if isActive}}
Active user
{{else}}
Inactive user
{{/if}}
{{#ifCond age '>=' 18}}
Adult
{{else}}
Minor
{{/ifCond}}
The ifCond helper supports these operators:
==, === - Equality!=, !== - Inequality<, <= - Less than (or equal)>, >= - Greater than (or equal)&& - Logical AND|| - Logical OR{{#if (and isActive isPremium)}}
Premium Active User
{{/if}}
{{#if (or isAdmin isModerator)}}
Staff Member
{{/if}}
{{#switch type}}
{{#case 'admin'}}
Administrator
{{/case}}
{{#case 'user'}}
Regular User
{{/case}}
{{#default}}
Unknown
{{/default}}
{{/switch}}
You can match multiple values in a single case:
{{#switch role}}
{{#case 'admin' 'moderator'}}
Staff Member
{{/case}}
{{#case 'user'}}
Regular User
{{/case}}
{{/switch}}
Use propertyExists to check if a property exists in any object:
{{#if (propertyExists items "email")}}
<th>Email</th>
{{/if}}
{{#each users}}
- {{name}} ({{email}})
{{/each}}
Iterate over unique values or unique values by property:
{{!-- Unique values --}}
{{#eachUnique tags}}
<span>{{this}}</span>
{{/eachUnique}}
{{!-- Unique by property --}}
{{#eachUnique items 'category'}}
Category: {{this.category}}
{{/eachUnique}}
Iterate over items selected by JSONPath (uniqueness determined by path, not value):
{{!-- Select array items (each has unique index in path) --}}
{{#eachUniqueJSONPath clay_model '$.types[*]'}}
<h2>{{this.name}} - {{this.category}}</h2>
{{/eachUniqueJSONPath}}
{{#times 3}}
Step {{inc @index}}
{{/times}}
Group items by a property:
{{#group posts by='category'}}
<h2>{{value}}</h2>
{{#each items}}
<p>{{title}}</p>
{{/each}}
{{/group}}
Check the type of values in your templates:
| Helper | Description | Example |
|---|---|---|
isArray |
Check if value is an array | {{#if (isArray items)}}Is array{{/if}} |
isString |
Check if value is a string | {{#if (isString name)}}Is string{{/if}} |
isNumber |
Check if value is a number | {{#if (isNumber count)}}Is number{{/if}} |
isBoolean |
Check if value is a boolean | {{#if (isBoolean active)}}Is bool{{/if}} |
isEmpty |
Check if value is empty | {{#if (isEmpty list)}}No items{{/if}} |
isNull |
Check if value is null | {{#if (isNull value)}}Is null{{/if}} |
isUndefined |
Check if value is undefined | {{#if (isUndefined opt)}}Not set{{/if}} |
Pretty-print objects as JSON:
<pre>{{{json this}}}</pre>
Convert markdown to HTML:
{{{markdown description}}}
{{!-- Increment (useful for 1-based indexes) --}}
Step {{inc @index}}
{{!-- Parse string to integer --}}
{{parseInt "42"}}
{{!-- Check if string contains value --}}
{{#if (includes email "@")}}Valid email format{{/if}}
{{!-- Check if string starts with value --}}
{{#if (startsWith filename "test_")}}Is test file{{/if}}
{{!-- Check if string ends with value --}}
{{#if (endsWith filename ".js")}}Is JS file{{/if}}
Split strings by delimiter and extract parts:
{{!-- Using split helper --}}
{{#each (split "foo,bar,baz" ",")}}
- {{this}}
{{/each}}
{{!-- Using splitAndUseWord to get specific part --}}
{{splitAndUseWord "user-profile-page" "-" 1}}
{{!-- Output: profile --}}
Reuse template fragments with partials. Define them in your generator:
{
"partials": ["partials/header.hbs"],
"steps": [...]
}
Use in templates:
{{> header}}
<div class="content">
{{name}}
</div>
Clay includes 47+ helpers across 8 categories:
clay_list_helpers tool to get detailed examples of all
helpers when working with AI assistants!
EJS templates embed JavaScript directly in the template using <% %> tags.
Use EJS when you need a few lines of computation inside an otherwise template-like file.
<% // JavaScript code (no output) %>
<%= expression %> <!-- Output (escaped) -->
<%- expression %> <!-- Output (unescaped) -->
<% const uniqueRefs = [...new Set(
(fields || []).filter(f => f.type === 'reference').map(f => f.reference)
)]; %>
<% uniqueRefs.forEach(ref => { %>
import { <%= helpers.pascalCase(ref) %> } from './<%= helpers.pascalCase(ref) %>';
<% }); %>
export class <%= helpers.pascalCase(name) %> {
<% (fields || []).forEach(f => { %>
<%= helpers.camelCase(f.name) %>: <%= f.type %>;
<% }); %>
}
In EJS templates, Clay helpers are available via the helpers object:
<%= helpers.pascalCase(name) %>
<%= helpers.pluralize(name) %>
<%= helpers.camelCase(fieldName) %>
For fully programmatic generation, use the CodeGenerator base class.
TypeScript templates are ideal for complex wiring files, DI containers, route registrations,
and any file where the generation logic is more code than template.
import { CodeGenerator, type RenderContext } from 'clay-generator/types';
export default class extends CodeGenerator {
render({ data, helpers, model }: RenderContext): string {
const { pascalCase, camelCase } = helpers;
return `export class ${pascalCase(data.name)} {}`;
}
}
The render() method receives a RenderContext with all available data:
| Property | Type | Description |
|---|---|---|
data |
Record<string, any> |
The selected model item (e.g., one entity from $.model.types[*]) |
helpers |
ClayHelpers |
All Clay helpers — pascalCase, camelCase, pluralize, etc. |
model |
Record<string, any> |
The full root model (equivalent to clay_model in Handlebars) |
parent |
Record<string, any> |
Parent object in the JSON hierarchy (equivalent to clay_parent) |
The render() method can be async. This enables database queries, API calls, or file reads during generation:
import { CodeGenerator, type RenderContext } from 'clay-generator/types';
export default class extends CodeGenerator {
async render({ data, helpers }: RenderContext): Promise<string> {
// Can do async operations — database queries, API calls, etc.
return `export class ${helpers.pascalCase(data.name)}Service {}`;
}
}
import { CodeGenerator, type RenderContext } from 'clay-generator/types';
export default class extends CodeGenerator {
render({ data, helpers }: RenderContext): string {
const { pascalCase, kebabCase } = helpers;
const types = data.model.types;
const imports = types
.map((t: any) =>
`import { ${pascalCase(t.name)}Controller } from './${pascalCase(t.name)}Controller';`
)
.join('\n');
const routes = types
.map((t: any) =>
` router.use('/${kebabCase(t.name)}', new ${pascalCase(t.name)}Controller().routes());`
)
.join('\n');
return `${imports}
export function registerRoutes(router: Router) {
${routes}
}`;
}
}
TypeScript templates can read the filesystem, making them ideal for barrel files, route registrations, and DI containers that must reflect what actually exists on disk. No stale exports for deleted files, no missing exports for new ones.
import { CodeGenerator, type RenderContext } from 'clay-generator/types';
import fs from 'fs';
import path from 'path';
export default class extends CodeGenerator {
render({ data, helpers }: RenderContext): string {
const dir = path.resolve('src/services');
const files = fs.readdirSync(dir)
.filter(f => f.endsWith('.ts') && f !== 'index.ts')
.map(f => f.replace('.ts', ''));
return files.map(f => `export * from './${f}';`).join('\n');
}
}
This pattern works for any file that aggregates across a directory:
TypeScript templates can import any npm package installed in your project. This enables powerful use cases like querying databases or fetching API specs during generation:
import { CodeGenerator, type RenderContext } from 'clay-generator/types';
import { Client } from 'pg';
export default class extends CodeGenerator {
async render({ data, helpers }: RenderContext): Promise<string> {
const client = new Client();
await client.connect();
const columns = await client.query(
'SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1',
[data.name]
);
await client.end();
// Generate code based on actual database schema
return `// Generated from database schema for ${data.name}\n...`;
}
}
| Engine | How to Access Helpers |
|---|---|
| Handlebars | {{pascalCase name}} — registered as Handlebars helpers |
| EJS | helpers.pascalCase(name) — via helpers object |
| TypeScript | helpers.pascalCase(name) — via RenderContext.helpers |
clay_model (Handlebars) or model (TS/EJS) to access the full model when needed