← Back to Home

Generators

Configure code generation steps

What are Generators?

Generators define a sequence of steps that transform your model into code. Each generator contains templates, partials, and configuration that tells Clay exactly what to generate.

Generator Structure

{
  "partials": ["partials/header.hbs"],
  "steps": [
    {
      "generate": "templates/{{kebabCase name}}.js",
      "select": "$.model.types[*]",
      "target": "src/models/"
    }
  ],
  "formatters": ["clay-generator-formatter-prettier"]
}

Generator Properties

partials

Array of Handlebars partial files to include:

"partials": [
  "partials/header.hbs",
  "partials/footer.hbs"
]

steps

Array of generation steps (run in order):

"steps": [
  { "runCommand": "mkdir -p src/models" },
  { "generate": "templates/{{name}}.js", "select": "$.model.types[*]" },
  { "copy": "foundation", "target": "src/foundation" }
]

formatters

Optional formatters to prettify generated code:

"formatters": ["clay-generator-formatter-prettier"]

Step Types

1. Generate (Template)

Generate files from templates. The engine is chosen with the engine field (Handlebars by default), not the file extension. The output file is named after the template's filename (rendered as Handlebars); target is an optional output subdirectory:

{
  "generate": "templates/{{kebabCase name}}.model.js",
  "select": "$.model.types[*]",
  "target": "src/models/",
  "touch": false
}

With different template engines (put the per-entity name and final extension in the template filename — don't add .hbs/.ejs, it is stripped):

// EJS — inline JavaScript logic
{ "generate": "templates/{{name}}Service.ts", "select": "$.model.types[*]", "target": "src/", "engine": "ejs" }

// TypeScript — fully programmatic via CodeGenerator class
{ "generate": "templates/index.ts", "select": "$.model", "target": "src/routes/", "engine": "ts" }

Parameters:

2. Copy

Copy files or directories:

{
  "copy": "foundation",
  "select": "$.model.types[*]",
  "target": "src/{{kebabCase name}}/foundation"
}

Parameters:

3. Run Command

Execute shell commands:

{
  "runCommand": "npm install",
  "npxCommand": false
}

With model data:

{
  "runCommand": "echo Generating {{name}}",
  "select": "$.model.types[*]"
}

Parameters:

Complete Example

{
  "partials": [
    "partials/license-header.hbs",
    "partials/imports.hbs"
  ],
  "steps": [
    {
      "runCommand": "mkdir -p src/models src/controllers src/routes"
    },
    {
      "generate": "templates/{{kebabCase name}}.model.js",
      "select": "$.model.types[*]",
      "target": "src/models/"
    },
    {
      "generate": "templates/{{kebabCase name}}.controller.js",
      "select": "$.model.types[*]",
      "target": "src/controllers/"
    },
    {
      "generate": "templates/{{kebabCase name}}.routes.js",
      "select": "$.model.types[*]",
      "target": "src/routes/"
    },
    {
      "generate": "templates/index.js",
      "target": "src/",
      "touch": true
    },
    {
      "copy": "config",
      "target": "config"
    },
    {
      "runCommand": "npm install"
    }
  ],
  "formatters": ["clay-generator-formatter-prettier"]
}

Creating a Generator

Initialize

clay init generator my-generator

This creates:

clay/generators/my-generator/
├── generator.json
└── templates/

Directory Structure

clay/generators/api/
├── generator.json          # Configuration
├── templates/              # Handlebars templates
│   ├── model.js
│   ├── controller.js
│   └── routes.js
├── partials/              # Reusable template parts
│   ├── header.hbs
│   └── imports.hbs
└── foundation/            # Files to copy
    └── config.js

Pre-Generation Checks

Generators can define preChecks that validate the fully resolved model (after includes and mixins) before any step runs. Prechecks are pure validators — model in, verdict out — and must not write files or mutate the model. Unlike steps and postGenerate hooks, a precheck failure aborts the generation for that model: nothing is rendered, written, copied, or executed. All prechecks run even if an early one fails, and every violation is aggregated into a single error.

{
  "preChecks": [
    { "run": "checks/cloudkit-invariants.ts" },
    { "runCommand": "node checks/naming.mjs", "select": "$.model.types[*]" }
  ],
  "steps": [...]
}

TypeScript Checks

Use the PreCheck base class, following the same pattern as PostGenerateHook. Import from clay-generator/types. Return a non-empty array of violation strings (or throw) to fail the check; return an empty array or nothing to pass.

import { PreCheck, type PreCheckContext } from 'clay-generator/types';

export default class extends PreCheck {
  check({ data }: PreCheckContext): string[] | void {
    if (!data.name) return ['every type needs a name'];
  }
}

Parameters (TypeScript check):

Parameters (command check):

PreCheckContext

The check() method receives a PreCheckContext with data (the selected item), helpers, model (the full root model), and parent — the same context shape as CodeGenerator.render().

Post-Generation Hooks

Generators can define postGenerate hooks that run after all steps complete and files are on disk. Hooks are best-effort — failures log warnings but never fail the generation.

TypeScript Hooks

The primary hook type uses the PostGenerateHook base class, following the same pattern as CodeGenerator. Import from clay-generator/types.

{
  "steps": [...],
  "postGenerate": [
    { "run": "hooks/fill-services.ts", "select": "$.model.types[*]", "onlyNewTouchFiles": true },
    { "runCommand": "prettier --write src/" }
  ]
}

Parameters (TypeScript hook):

Parameters (command hook):

HookContext

The run() method receives a HookContext with:

Property Description
data The selected model item
helpers Clay helpers (pascalCase, camelCase, etc.)
model The full root model
parent Parent object in the JSON hierarchy
touchFiles Only files newly created this run
outputDir Generator output directory
generatedFiles All files generated this run

Example: AI-Assisted Fill-in with Claude

Use Claude Code headless mode to fill in touch files after generation. Each hook spawns a focused Claude instance with tight context — the interface contract and entity data — rather than the entire project. Runs on your existing Claude subscription.

import { PostGenerateHook, type HookContext } from 'clay-generator/types';
import { execSync } from 'child_process';
import fs from 'fs';

export default class extends PostGenerateHook {
  async run({ data, helpers, touchFiles, outputDir }: HookContext): Promise<void> {
    const { pascalCase } = helpers;

    // Read the generated interface for context
    const iface = fs.readFileSync(
      `${outputDir}/src/services/I${pascalCase(data.name)}Service.ts`, 'utf-8'
    );

    for (const file of touchFiles) {
      const prompt = `Implement ${pascalCase(data.name)}ServiceImpl following this interface:\n${iface}`;
      execSync(`claude -p '${prompt}'`, { cwd: outputDir, timeout: 60000 });
    }
  }
}

Execution Order

Best Practices

Getting Generators

Clay uses a local-first approach. All generators are stored in your project's clay/generators/ directory.

Create a New Generator

Start from scratch with a template:

clay init generator my-api-generator

Copy from a Local Path

Copy a generator from another location on your machine:

clay generator add /path/to/my-generator
clay generator add ../other-project/generators/my-generator

Clone from the Registry

Browse available generators and clone them to your project:

clay generator list-available
clay generator add clay-model-documentation

Clone from GitHub

Clone any generator repository. It will be copied to your local clay/generators/ directory:

clay generator add https://github.com/user/my-generator

Reference in Your Model

Generators are referenced by their path to the generator.json file:

{
  "name": "my-app",
  "generators": [
    "generators/my-api-generator/generator.json",
    "generators/forms/generator.json"
  ],
  "model": {...}
}
💡 Pro Tip: After adding a generator, customize it for your project! The generator files are in clay/generators/ and are part of your codebase. Use clay watch to see changes immediately!

Sharing Generators

Since generators are stored locally in your project, sharing them is easy: