Antigravity × Custom Code Generator — Building Project-Specific Code Generation Pipelines with Template Engines + AI Agents
Learn how to build custom code generators by combining Antigravity's AI agents with template engines. Master Hygen, Plop, and custom TypeScript scaffolding integrated with AGENTS.md to elevate your team's code quality and productivity.
Setup and context — Why You Need Your Own Code Generator
As projects grow, you'll find yourself repeatedly creating the same file structures — API endpoints, React components, database migrations, test files. When done manually each time, it's not just slow; it leads to subtle inconsistencies as different team members interpret patterns differently.
Antigravity's AI agent can generate code that follows your project's conventions, but relying solely on prompts means repeating the same instructions and dealing with inconsistent outputs. This is where the combination of template engines and AI agents becomes powerful.
Template engines handle the "structure" while AI agents provide "context-aware logic." This two-layer architecture lets you build code generation pipelines tailored specifically to your project. In this guide, we'll walk through comparing major template engines, integrating them with Antigravity's AGENTS.md and custom rules, and building a fully working TypeScript generator from scratch.
Choosing a Template Engine — Hygen vs. Plop vs. Custom Scripts
The first decision when building a code generator is which template engine to use. Let's compare the three main approaches.
Hygen — File-Based Simplicity
Hygen is a generator that works by simply placing template files in your file system. It has a low learning curve and is well-suited for small to mid-sized projects.
# Install and set up Hygennpm install -g hygenhygen init self# Create a template# _templates/component/new/index.tsx.ejs.t
// _templates/component/new/index.tsx.ejs.t---to: src/components/<%= name %>/<%= name %>.tsx---import React from 'react';interface <%= name %>Props { /** Component description */ children?: React.ReactNode;}/** * <%= name %> component * Auto-generated: <%= new Date().toISOString().split('T')[0] %> */export const <%= name %>: React.FC<<%= name %>Props> = ({ children }) => { return ( <div className="<%= h.changeCase.paramCase(name) %>"> {children} </div> );};export default <%= name %>;
Hygen's strength is that templates live as plain files in your repository, making code review and version control straightforward.
Plop — Interactive Prompt-Driven Generation
Plop generates code through interactive prompts, gathering input from the user. It excels when you need complex conditional branching.
As a general rule: choose Hygen for a handful of simple templates, Plop when you need interactive generation flows, and custom TypeScript scripts when you need complete control.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Master the integration patterns between template engines (Hygen/Plop) and AI agents systematically
✦Build organization-wide code generation pipelines using AGENTS.md and custom rules
✦Get working TypeScript generator implementations with complete testing strategies
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Integration Architecture with Antigravity AI Agents
Template engines alone produce "static scaffolds." By integrating Antigravity's AI agent, you unlock context-aware, intelligent code generation.
The Two-Layer Architecture
┌─────────────────────────────────────────────┐
│ User Request (Natural Language) │
│ "Create an API endpoint with auth and │
│ validation" │
└───────────────┬─────────────────────────────┘
│
┌───────────▼───────────┐
│ Antigravity AI Agent │ ← Context + Judgment
│ (References AGENTS.md)│
└───────────┬───────────┘
│ Determines parameters
┌───────────▼───────────┐
│ Template Engine │ ← Structural guarantee
│ (Hygen / Plop) │
└───────────┬───────────┘
│ File generation
┌───────────▼───────────┐
│ AI Post-Processing │ ← Context-specific logic
│ (Type inference, etc.)│
└───────────────────────┘
In this two-layer architecture, templates generate the "skeleton" and the AI agent provides the "flesh." Templates guarantee file naming conventions, directory structure, import path consistency, and boilerplate uniformity. The AI agent handles business logic implementation, edge case handling, type definition optimization, and documentation comments.
Defining Generator Rules in AGENTS.md
By embedding code generation guidelines in Antigravity's AGENTS.md, the AI agent understands your templates' intent when generating or modifying code.
<!-- AGENTS.md (excerpt) -->## Code Generation Guidelines### Required Rules for File Generation1. Create new components via `scripts/generate.ts`2. When creating files directly, follow existing template patterns3. Always run `npm run lint:fix` after generation### Naming Conventions- Components: PascalCase (e.g., UserProfile)- File names: kebab-case (e.g., user-profile.tsx)- Test files: *.test.ts / *.spec.ts- Hooks: use + PascalCase (e.g., useUserProfile)### Template Locations- Components: `_templates/component/`- API routes: `_templates/api/`- Hooks: `_templates/hook/`- Services: `_templates/service/`### Post-Generation Checklist- [ ] No TypeScript type errors- [ ] No ESLint warnings- [ ] Corresponding test file exists- [ ] Added to index.ts exports
With these definitions in AGENTS.md, when a team member tells Antigravity to "create a new component," the agent automatically follows the generator's rules.
Implementation — Building a React Component Generator
Let's build a working component generator in TypeScript, showing the template-AI integration in practice.
Controlling Generation Quality with Custom Rule Files
By placing custom rules in Antigravity's .antigravity/rules/ directory, you can precisely control the guidelines the AI agent references during code generation.
<!-- .antigravity/rules/code-generation.md --># Code Generation Rules## Component Generation- All components must use the `React.FC<Props>` type- Props interfaces must always be exported- No default exports (named exports only)- When using CSS Modules, class names follow BEM notation## Test Generation- Use Vitest as the test framework- Follow `describe` > `it` nesting structure- Use `@testing-library/react`'s `render` and `screen`## API Route Generation- Zod schema validation is mandatory- Error responses must follow RFC 7807 format- Always include rate limiting middleware
With this rule file in place, here's how Antigravity's agent behaves:
User: "Create a UserSettings component"
Antigravity Agent's thought process:
1. Load code-generation.md rules
2. Check _templates/component/ templates
3. Analyze existing component patterns
4. Generate with React.FC<Props> + named exports per rules
5. Simultaneously generate Vitest test file
Template Versioning and Evolution Strategy
Templates aren't "set and forget." You need systems to evolve them alongside your project.
Template Validation
Prepare a script to verify templates aren't broken, and run it in CI.
Before modifying a template, an impact analysis script helps assess which generated files would be affected.
// scripts/analyze-template-impact.tsimport { readdir, readFile } from 'fs/promises';import { join, relative } from 'path';async function analyzeImpact(templateName: string): Promise<void> { // Track template versions through header comments // in generated files const srcDir = join(__dirname, '../src'); const generatedFiles = await findGeneratedFiles(srcDir); console.log(`Files generated from template "${templateName}":`); console.log(`Total: ${generatedFiles.length} files`); for (const file of generatedFiles) { const relPath = relative(process.cwd(), file.path); console.log(` ${relPath} (v${file.templateVersion})`); }}interface GeneratedFileInfo { path: string; templateVersion: string; generatedAt: string;}async function findGeneratedFiles(dir: string): Promise<GeneratedFileInfo[]> { // Generated files contain this header: // // @generated by template v1.2.0 at 2026-03-30 const results: GeneratedFileInfo[] = []; const entries = await readdir(dir, { withFileTypes: true, recursive: true }); for (const entry of entries) { if (!entry.isFile() || !entry.name.endsWith('.tsx')) continue; const filePath = join(dir, entry.name); const content = await readFile(filePath, 'utf-8'); const match = content.match( /@generated by template v([\d.]+) at ([\d-]+)/ ); if (match) { results.push({ path: filePath, templateVersion: match[1], generatedAt: match[2], }); } } return results;}
Dynamic Template Extension with AI Agents
Static templates can't handle every case. For instance, generating TypeScript interfaces from an external API's schema requires intelligence beyond what templates offer. This is where Antigravity's AI agent steps in.
Pattern 1: Schema-Driven Code Generation
// scripts/schema-driven-generate.tsimport { readFile, writeFile } from 'fs/promises';import { join } from 'path';/** * Generate API clients from an OpenAPI schema. * Antigravity's agent analyzes the schema and * generates type-safe client code. */async function generateFromSchema( schemaPath: string): Promise<void> { const schema = JSON.parse( await readFile(schemaPath, 'utf-8') ); // Generate client functions for each endpoint for (const [path, methods] of Object.entries(schema.paths)) { const endpoint = methods as Record<string, unknown>; for (const [method, config] of Object.entries(endpoint)) { const operationId = (config as { operationId?: string }).operationId; if (!operationId) continue; // Instructions for AI agent // Generates type definitions following AGENTS.md rules const typeDefinition = generateTypeFromSchema( config as Record<string, unknown> ); console.log( `Generated: ${method.toUpperCase()} ${path} -> ${operationId}` ); } }}function generateTypeFromSchema( config: Record<string, unknown>): string { // The AI agent dynamically fills this part // Templates provide only the skeleton return '// AI agent generates type definitions';}
Pattern 2: Learning Patterns from Existing Code
Antigravity's agent can analyze your existing codebase and learn project-specific patterns. Leverage this to dynamically improve your templates.
<!-- Example instruction for Antigravity -->Analyze src/components/ in this project and extractthe following patterns:1. Most frequently used React hook combinations2. Prop naming patterns (on* event handlers, etc.)3. Error handling patterns4. Comment / JSDoc conventionsOutput the results to _templates/component/patterns.json.
This approach makes templates "living documents" that continuously adapt to your project's actual patterns.
Testing Strategy — Testing the Generator Itself
A code generator produces other code, so you need a testing strategy that verifies the "generated code is correct."
The first few weeks after adding a generator feel great. The trouble starts later.
As an indie developer shipping and maintaining my own apps, I have four projects sharing the same component template. About three months in, I noticed that a component generated in one project was written noticeably differently from its counterpart in another.
The template wasn't the culprit. The edits people (and agents) made after generation had never flowed back into the template. The template was still at v1 while the actual code had quietly moved to something like v1.3.
I call this drift. The hard part of running a generator turned out not to be writing the first template — it was noticing drift on an ongoing basis.
Detecting Drift Mechanically
The approach is simple: take each generated file, re-render it through the current template, and diff the result against what's on disk. Whatever differs has drifted away from the template.
Normalize formatting before comparing. Skip that step and every file shows up as changed because of blank lines and quote styles, which makes the report useless.
// scripts/detect-drift.tsimport { readFile, readdir } from 'fs/promises';import { join, relative } from 'path';import Handlebars from 'handlebars';interface DriftReport { file: string; templateVersion: string; changedLines: number; totalLines: number; ratio: number;}/** * Flatten formatting noise. * Without this, blank lines and quote style alone mark every file as drifted. */function normalize(src: string): string { return src .replace(/\/\/ @generated by template v[\d.]+ at [\d-]+\n/g, '') .replace(/'/g, '"') .replace(/;\s*$/gm, '') .split('\n') .map((l) => l.trimEnd()) .filter((l) => l.length > 0) .join('\n');}function countChangedLines(a: string, b: string): number { const setB = new Set(b.split('\n')); return a.split('\n').filter((l) => !setB.has(l)).length;}export async function detectDrift( srcDir: string, templatePath: string, contextOf: (file: string) => Record<string, unknown>): Promise<DriftReport[]> { const templateSource = await readFile(templatePath, 'utf-8'); const template = Handlebars.compile(templateSource); const entries = await readdir(srcDir, { withFileTypes: true, recursive: true, }); const reports: DriftReport[] = []; for (const entry of entries) { if (!entry.isFile() || !entry.name.endsWith('.tsx')) continue; const filePath = join(entry.parentPath ?? srcDir, entry.name); const actual = await readFile(filePath, 'utf-8'); // Files not marked as generated are out of scope const stamp = actual.match(/@generated by template v([\d.]+)/); if (!stamp) continue; const expected = template(contextOf(filePath)); const na = normalize(actual); const ne = normalize(expected); const totalLines = ne.split('\n').length; const changedLines = countChangedLines(na, ne); reports.push({ file: relative(process.cwd(), filePath), templateVersion: stamp[1], changedLines, totalLines, ratio: totalLines === 0 ? 0 : changedLines / totalLines, }); } return reports.sort((x, y) => y.ratio - x.ratio);}// Sample output:// src/components/user-profile/user-profile.tsx v1.0 42/58 lines (72%)// src/components/data-table/data-table.tsx v1.0 11/61 lines (18%)// src/components/badge/badge.tsx v1.2 2/47 lines ( 4%)
Triage by Drift Ratio
Trying to fix everything the report surfaces will stall you. I bucket results by ratio instead.
Drift ratio
What it usually means
What I do
Under 5%
Formatting and comment noise
Ignore it; raise the detection threshold
5–30%
Additions specific to that one file
Leave it; drop the generated marker so it exits the managed set
Over 30%
The template itself is probably stale
Read the diff; fold anything shared back into the template
The first run across four projects and 61 generated files flagged 9 files over 30%. Reading them, 6 contained the same addition: a loading-state branch and an aria-busy attribute.
That's a template gap, not a discipline problem. I folded those 6 edits into template v1.1, and the same scan a month later returned zero files over 30%.
What actually paid off wasn't the generator — it was having a mechanism that tells me the template has gone stale. I run it monthly.
Removing Templates Is Harder Than Adding Them
Running drift detection surfaced something else: I simply had too many templates.
Across those four projects, the collection had grown to nine — component, API route, hook, service, middleware, job, migration, E2E test, and config file.
Counting three months of generation logs, only six were actually in use. The remaining three (middleware, job, config file) had each been invoked exactly once in that window.
A template you use once a quarter leaves you with maintenance cost and nothing else. Worse, low-frequency templates go stale without anyone noticing, so they're broken the moment you finally need them. The job template stopped working the day I renamed a Handlebars helper, and nobody found out for two months.
I deleted those three and switched to having the agent reference an existing file when the need comes up. Fewer templates also means faster validation runs and a smaller drift-detection surface.
My rule of thumb now: does it get invoked at least three times a quarter? Below that, a single line in AGENTS.md is cheaper than a template.
Where Agents Tend to Break Things
Ask an Antigravity agent to add a component and it follows the template path faithfully. The gaps appear wherever the work doesn't end with a single file.
Three failures kept recurring in my setup:
Missing barrel export. The component itself generates correctly, but the index.ts export never gets added. The build passes and only the import fails, so you find out late.
Test names drifting off the rule. Even with AGENTS.md specifying a naming convention, the agent's defaults sometimes win. Adding more instruction text didn't fix it.
One-sided schema updates.route.ts gets updated while schema.ts stays on the old shape.
For 1 and 3, adding a check beat adding instructions. One script runs after generation and again at pre-push.
// scripts/verify-generated.tsimport { readFile, readdir } from 'fs/promises';import { join } from 'path';interface Violation { kind: 'missing-export' | 'schema-stale'; file: string; detail: string;}export async function verifyGenerated( componentsDir: string): Promise<Violation[]> { const violations: Violation[] = []; const dirs = await readdir(componentsDir, { withFileTypes: true }); for (const dir of dirs) { if (!dir.isDirectory()) continue; const barrelPath = join(componentsDir, dir.name, 'index.ts'); let barrel = ''; try { barrel = await readFile(barrelPath, 'utf-8'); } catch { violations.push({ kind: 'missing-export', file: barrelPath, detail: 'index.ts does not exist', }); continue; } const files = await readdir(join(componentsDir, dir.name)); const impl = files.filter( (f) => f.endsWith('.tsx') && !f.endsWith('.test.tsx') ); for (const f of impl) { const base = f.replace(/\.tsx$/, ''); if (!barrel.includes(`./${base}`)) { violations.push({ kind: 'missing-export', file: barrelPath, detail: `${base} is not re-exported`, }); } } } return violations;}// package.json:// "postgenerate": "tsx scripts/verify-generated.ts"// call the same script from .git/hooks/pre-push
Item 2 wasn't worth a check, so I removed the rule instead. Leaving a rule in AGENTS.md that never gets followed erodes how well the remaining rules land. Write instructions you can verify are being followed. Once I made that cut, the number of times I had to hand-edit generated output dropped visibly.
Start with one small template — a single component type is plenty. Add one or two lines of guidance to AGENTS.md and .antigravity/rules/, then watch whether generation stays stable.
From there, pick at least one of the three safeguards covered here and add it early: template validation, generated-output verification, and drift detection. A mechanism that tells you the template has aged pays off more than another template does.
As a next step, pick one generated file you already have, re-render it through its template, and look at the diff. Just knowing how many lines have moved tells you what belongs back in the template.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.