Authoring a template
A practical walkthrough for creating a new nanohype template, from empty directory to validated, catalog-ready artifact.
1. Prerequisites
Before you begin:
- Read the Template Contract Specification. This guide walks you through the contract; the spec is the authoritative reference.
- Install dependencies for validation:
cd <catalog-root>npm installThis pulls in ajv-cli and ajv-formats, which power schema validation via npm run validate:schema.
- A text editor and basic familiarity with YAML.
2. Quick Start
Every template lives in templates/<name>/ and contains three things:
templates/ my-template/ template.yaml # manifest (required) skeleton/ # output file tree (required, may be empty) README.md # documentation (recommended)Create the directory and files:
mkdir -p templates/my-template/skeletontouch templates/my-template/template.yamltouch templates/my-template/README.mdThe directory name (my-template) must be kebab-case and must exactly match the name field in template.yaml.
3. Writing template.yaml
The manifest is the heart of the template. Build it up one section at a time.
3.1 Start with the Header
Every manifest begins with the API version:
apiVersion: nanohype/v1This is always nanohype/v1. Consumers reject manifests with any other value.
3.2 Metadata
name: my-templatedisplayName: 'My Template'description: > A one-to-three sentence description of what this template scaffolds and why someone would reach for it.version: '0.1.0'license: Apache-2.0tags: [python, cli, async]Field rules:
name– Kebab-case, matches the directory name. Pattern:^[a-z][a-z0-9-]*$.displayName– Human-readable. Used in UIs and listings.description– Multi-line is fine (use YAML>or|folding). Explain what the scaffolded output looks like.version– Valid semver. Bump this when the template’s output changes meaningfully.license– SPDX identifier for the scaffolded output, not the template itself. Optional.tags– At least one. Lowercase, kebab-case. Used for search and filtering.
3.3 Variables
Variables are the inputs users provide when scaffolding. Declare them as an ordered array:
variables: - name: ProjectName type: string placeholder: '__PROJECT_NAME__' description: 'Name of the project' prompt: 'Project name' default: 'myapp' required: true validation: pattern: '^[a-z][a-z0-9-]*$' message: 'Must be lowercase kebab-case'Every variable needs four fields: name, type, placeholder, and description. Everything else is optional (but prompt, default, and validation are strongly recommended).
Variable names are PascalCase. Pattern: ^[A-Z][a-zA-Z0-9]*$.
3.4 Variable Types
There are four types. Here is a complete example of each.
string – Free-form text input:
- name: GoModule type: string placeholder: '__GO_MODULE__' description: 'Full Go module path' default: 'github.com/example/${ProjectName}' required: true validation: pattern: '^[a-z][a-z0-9./-]*$' message: 'Must be a valid Go module path'bool – True or false. Primarily used with conditionals to include or exclude files:
- name: IncludeDocker type: bool placeholder: '__INCLUDE_DOCKER__' description: 'Include Dockerfile and docker-compose.yaml' default: falseenum – One value from a fixed list. Requires the options field:
- name: LogLevel type: enum placeholder: '__LOG_LEVEL__' description: 'Default log level' default: 'info' options: ['debug', 'info', 'warn', 'error']int – An integer value:
- name: HttpPort type: int placeholder: '__HTTP_PORT__' description: 'Default HTTP port' default: 80803.5 Defaults and Cross-References
Defaults are optional but improve the user experience. A default must match the variable’s type (a string for string/enum, a boolean for bool, an integer for int).
Defaults can reference other variables using ${VariableName} syntax:
- name: ProjectName type: string placeholder: '__PROJECT_NAME__' description: 'Project name' default: 'myapp'
- name: BinaryName type: string placeholder: '__BINARY_NAME__' description: 'Output binary name' default: '${ProjectName}'Cross-references are resolved after the user provides all explicit values. Circular references are invalid – consumers will reject them.
Variable ordering matters. Variables are presented to users in array order. Put foundational variables (like project name) first so that later variables can reference them in defaults.
3.6 Validation Patterns
The validation.pattern field takes an ECMAScript regex. Consumers treat it as anchored (the entire value must match). Always pair it with a message:
validation: pattern: '^[a-z][a-z0-9-]*$' message: 'Must be lowercase kebab-case starting with a letter'Validation runs after defaults are resolved, before placeholder substitution. It applies to string and int types. There is no validation for bool (it is inherently constrained) and enum values are validated against options automatically.
4. Building the Skeleton
The skeleton/ directory contains the files and directories that will appear in the scaffolded output, exactly as they should look – but with placeholder tokens where variable values belong.
4.1 Placeholder Convention
Placeholders are literal strings declared in each variable’s placeholder field. There is no mandated syntax. The convention in this catalog is double-underscore wrapping: __LIKE_THIS__.
In file content:
module __GO_MODULE__
go 1.22In file and directory names:
skeleton/ cmd/ __PROJECT_NAME__/ main.goWhen a user scaffolds with ProjectName = "hype", the output is cmd/hype/main.go.
4.2 Substitution Rules
- Find-and-replace is textual and non-recursive. A resolved value that contains another placeholder string will not trigger a second pass.
- All occurrences of a placeholder are replaced. There is no escape mechanism.
- Placeholders must be unique across all variables. No placeholder may be a substring of another placeholder in the same template.
4.3 Skeleton Quality
Keep skeleton files as close to real, runnable code as possible. After scaffolding with sensible defaults, the output should:
- Compile or pass a syntax check
- Have valid configuration files
- Include reasonable
.gitignore,Makefile, or equivalent for the ecosystem - Work as a starting point someone can
git initand build on immediately
Avoid leaving TODO comments where real code should be. If a section needs user customization, put a working default in place and note it in the template README.
5. Conditionals
Conditionals let you include or exclude files based on a bool variable.
variables: - name: IncludeDocker type: bool placeholder: '__INCLUDE_DOCKER__' description: 'Include Docker configuration' default: false
conditionals: - path: 'Dockerfile' when: IncludeDocker - path: 'docker-compose.yaml' when: IncludeDocker - path: '.dockerignore' when: IncludeDockerRules:
pathis relative toskeleton/. It can be a file or a directory (the entire subtree is included or excluded).whenmust reference abool-typed variable.- Files not mentioned in any conditional are always included.
- A file must not appear in more than one conditional. If you need complex logic, introduce an additional
boolvariable. - Placeholder substitution still applies to conditionally-included files.
6. Hooks
Hooks are shell commands that run before or after skeleton expansion.
hooks: pre: [] post: - name: install-dependencies description: 'Install Python dependencies' run: 'pip install -r requirements.txt' workdir: '.' - name: init-git description: 'Initialize git repository' run: 'git init && git add -A' workdir: '.'6.1 Lifecycle
1. Validate manifest2. Collect variable values3. Validate variable values4. Check prerequisites5. Run pre hooks (in order)6. Copy skeleton with substitution and conditional filtering7. Run post hooks (in order)6.2 Advisory Execution Model
Hooks are advisory, not mandatory. Consumers will:
- Display each hook’s name, description, and command to the user
- Require explicit confirmation before running
- Allow users to skip all hooks (e.g.
--no-hooks)
Design your template so it produces correct output even if hooks are skipped. Hooks should handle convenience setup (installing dependencies, formatting code, initializing git) rather than structural correctness.
6.3 Environment
Hooks receive NANOHYPE_TEMPLATE_NAME and NANOHYPE_OUTPUT_DIR as environment variables. Resolved template variables may be available as NANOHYPE_VAR_<Name> but this is not guaranteed – do not depend on it without checking.
7. Composition and Prerequisites
7.1 Composition
Composition is advisory metadata that helps users discover related templates.
composition: pairsWith: [eval-harness, docker-deploy] nestsInside: [monorepo]pairsWith– Templates designed to work alongside this one.nestsInside– Templates within whose output this template can be scaffolded as a subdirectory.
These are suggestions, not enforcement. Consumers may surface them after scaffolding. Referenced templates do not need to exist in the catalog yet.
7.2 Prerequisites
Prerequisites declare external tools the scaffolded output or hooks depend on.
prerequisites: - name: go version: '>=1.22' purpose: 'Go compilation and module management' optional: false - name: goreleaser purpose: 'Automated release builds (only if release config is included)' optional: true- Required prerequisites (
optional: false, the default) cause consumers to warn and potentially block. - Optional prerequisites cause a warning but never block.
- Always include
purposeso users understand the impact of a missing tool. versionis a semver range. Version checking is best-effort on the consumer side.
8. Validating Your Template
8.1 Schema Validation
Run the JSON Schema validator against all templates in the catalog:
npm run validate:schemaThis uses ajv-cli to validate every templates/*/template.yaml against schemas/template.schema.json.
8.2 Structural Validation
Run the full validation script, which checks both schema conformance and structural invariants:
./scripts/validate.shOr validate a single template:
./scripts/validate.sh templates/my-templateThe structural checks enforce rules that JSON Schema alone cannot express:
- Every
conditionals[].whenreferences abool-typed variable - All placeholders are unique and no placeholder is a substring of another
- Every
enumvariable has a non-emptyoptionsarray - Default values match their variable’s type
- Default cross-references (
${VariableName}) do not form cycles - The directory name matches the
namefield
8.3 Manual Smoke Test
Before submitting, mentally walk through a scaffolding run:
- Pick values for every variable (or use defaults).
- Check that every placeholder in the skeleton would be replaced.
- Verify conditionals would include/exclude the right files.
- Confirm the resulting file tree makes sense as a real project.
9. Template README
Every template should include a README.md alongside template.yaml. Sections, in order:
- What you get – a summary of the scaffolded file tree and what each piece does.
- Variables – a table listing each variable, its type, default, and what it controls. This duplicates information from
template.yamlin a human-scannable format. - Project layout – the directory tree with explanations of each component.
- Pairs with – links to complementary templates (matches
composition.pairsWith). - Nests inside – where this template can be scaffolded within (matches
composition.nestsInside).
The README is for humans browsing the catalog. The manifest is for tools.
10. Common Patterns
Use descriptive placeholder names
Prefer __GO_MODULE__ over __VAR2__. Placeholders appear in skeleton source code – someone reading the skeleton should understand what each placeholder means without cross-referencing the manifest.
Put the most important variable first
The variables array is ordered. Lead with the project name or primary identifier. Follow with values that other defaults depend on. Put optional/niche variables last.
One bool per conditional group
If you have a group of related files that should be included or excluded together (e.g. Docker files, CI configuration, release automation), use a single bool variable and multiple conditional entries pointing to it.
Keep skeletons minimal but complete
Include enough files to produce something that works out of the box. Do not include every possible configuration file an advanced user might want. The template is a starting point, not a comprehensive scaffold.
Use cross-references to reduce user input
If the binary name is usually the same as the project name, default it to ${ProjectName}. Users who want something different can override it; everyone else provides one fewer value.
Make hooks idempotent
Since hooks may be re-run (or skipped and run manually later), write them so they are safe to execute more than once. go mod tidy and npm install are naturally idempotent. git init in an existing repo is a no-op. Prefer commands with this property.
Test with realistic values
When validating your template, use values that look like a real project – not test and foo. Real values surface issues with placeholder collisions, path lengths, and code correctness that synthetic values miss.
Version intentionally
Start at 0.1.0. Bump the patch version for skeleton fixes, the minor version for new variables or structural changes, and the major version for breaking changes to the variable interface that would change existing users’ workflows.
Provider registry pattern
When a template has a pluggable seam (LLM provider, database driver, auth provider, etc.), use the registry pattern:
- Define an interface –
types.ts(TypeScript) or a Protocol class (Python) - Create a registry –
registry.tswithregisterProvider(),getProvider(),listProviders() - Self-registering implementations – each provider file imports
registerProviderand calls it at the bottom - Barrel import –
index.tsimports all provider files to trigger registration, then re-exports the registry API
Use type: string (not enum) for the variable so users can add custom providers without modifying template.yaml. The default value names a built-in provider:
variables: - name: LlmProvider type: string placeholder: '__LLM_PROVIDER__' description: 'LLM provider (built-in: anthropic, openai — or any registered custom provider)' default: 'anthropic'Adding a new provider to a scaffolded project is then: create a file, implement the interface, self-register, add one import line.
Module templates
Templates prefixed with module- are composable modules designed to be layered into other projects rather than used standalone. They follow the same template.yaml contract but their skeletons produce a subdirectory (e.g., src/auth/, src/db/) rather than a full project.
Module templates should:
- Use the same registry pattern for their pluggable seams
- Export a clean public API via a barrel
index.ts - Include a standalone
package.jsonso they can be validated independently - Reference the templates they complement via
composition.pairsWith
Maintained in the catalog repo as docs/authoring-guide.mdand rendered here from it.