Building Generators

Creating a Generator

A generator is a directory with a workflow.yaml entry point plus any templates, actions, and scripts it references. This page walks through everything from a minimal example to a production-ready multi-job generator.

Folder structure

xo-button/
├── workflow.yaml             ← entry point (or workflows/add.yaml, workflows/create.yaml)
├── templates/
│   └── button.tsx            ← files copied / rendered into the user's project
├── actions/
│   ├── add-barrel.yaml       ← composite action (reusable steps)
│   └── validate-name.js      ← script action (custom Node.js logic)
└── scripts/
    └── post-install.sh

Use workflow.yaml at the root for a single trigger. Use workflows/add.yaml + workflows/create.yaml for multiple triggers from the same repo:

xo-stripe/
├── workflows/
│   ├── add.yaml        ← xo add @github/my-org/xo-stripe
│   └── create.yaml     ← xo create @github/my-org/xo-stripe
└── templates/
    └── stripe-route.ts

Minimal example

The smallest valid generator — one input, one job, one step:

workflow.yamlyaml
# workflow.yaml
name: ui/button
on: [add]
description: Add a reusable Button component

inputs:
  componentName:
    prompt: "Component name?"
    default: Button

jobs:
  scaffold:
    steps:
      - uses: xo/copy
        with:
          from: templates/button.tsx
          to: "src/components/{{ inputs.componentName }}.tsx"
Inside templates and with: values, use {{ inputs.name }} for prompt answers, {{ steps.id.outputs.key }} for step outputs, and Handlebars helpers like pascalCase, kebabCase, includes.

workflow.yaml reference

FieldTypeRequiredDescription
namestringYesUnique identifier — conventionally owner/repo
ontrigger[]YesWhich xo commands trigger this: "add", "create", "run"
descriptionstringNoShort description shown in the CLI
detectsDetectRule[]NoFile/package checks that must pass before running
requiresstring[]NoGenerator names that must already be applied
conflictsstring[]NoGenerator names that must NOT be present
providesstring[]NoLogical capability tags this generator declares
inputsRecord<string, Input>NoInteractive prompts collected before jobs run
jobsRecord<string, Job>YesNamed jobs containing ordered steps

Defining inputs

Inputs are collected interactively before any jobs run. Answers are available in templates and step with: values as {{ inputs.name }}.

inputs:
  appName:
    prompt: "App display name?"
    type: text
    required: true

  framework:
    prompt: "Framework?"
    type: select
    choices:
      - { value: react, label: "React" }
      - { value: vue,   label: "Vue" }
    default: react

  features:
    prompt: "Features to include?"
    type: multiselect
    choices:
      - { value: auth,    label: "Auth" }
      - { value: storage, label: "Storage" }

  confirm:
    prompt: "Proceed?"
    type: confirm
    default: true
TypeRenders asExtra fields
textFree-text fieldpattern, min, max, required
confirmYes / No questiondefault
selectSingle-choice listchoices, default
multiselectMulti-choice checkboxeschoices

Input validation

Text inputs support pattern, min, and max. Validation runs inline — a clear error is shown and the prompt is re-displayed on failure.

inputs:
  projectName:
    prompt: "Project name?"
    type: text
    required: true
    pattern: "^[a-z][a-z0-9_]*$"   # regex — must start with a lowercase letter
    min: 2                            # minimum character length
    max: 50                           # maximum character length

  version:
    prompt: "Version?"
    type: text
    pattern: "^\d+\.\d+\.\d+$"   # semver — e.g. 1.0.0
FieldTypeDescription
patternstringECMAScript regex the value must satisfy
minnumberMinimum character length
maxnumberMaximum character length

Jobs and steps

Jobs run in topological order based on needs: dependencies. Each job contains an ordered list of steps. Use if: on any step to conditionally skip it.

jobs:
  detect:
    steps:
      - uses: xo/detect-pm
        id: pm
        parallel: true
      - uses: xo/pkg-installed
        id: hasNext
        parallel: true
        with:
          pkg: next

  install:
    needs: [detect]
    steps:
      - uses: xo/install-pkg
        with:
          pkg: stripe

  configure:
    needs: [install]
    steps:
      - if: "steps.hasNext.outputs.installed == true"
        uses: xo/copy
        with:
          from: templates/next-route.ts
          to: app/api/stripe/route.ts

      - run: "{{ steps.pm.outputs.value }} db:push"

Parallel steps

Add parallel: true to steps that can run concurrently. Consecutive parallel steps are grouped into a batch and executed via Promise.all. The batch completes before the next non-parallel step begins.

steps:
  - uses: xo/detect-pm
    id: pm
    parallel: true
  - uses: xo/detect-lang
    id: lang
    parallel: true
  - uses: xo/file-exists    # waits for both above to finish
    id: hasPrisma
    with:
      path: prisma/schema.prisma
Parallelise independent detection steps to reduce total workflow time — particularly useful when each check makes a separate file-system or network call.

Handlebars templates

Template files (used with xo/template) support the full Handlebars syntax. Variables come from inputs, step outputs, and the project config.

# templates/app.tsx
import { Provider } from '{{ inputs.stateManagement === "riverpod" ? "flutter_riverpod" : "flutter_bloc" }}';

// Handlebars helpers work in template content:
// {{ pascalCase inputs.appName }}
// {{ kebabCase inputs.projectName }}
// {{#if (includes inputs.features "auth")}} ... {{/if}}
HelperInputOutput
pascalCasemy-componentMyComponent
camelCasemy-componentmyComponent
kebabCaseMyComponentmy-component
snakeCaseMyComponentmy_component
capitalizehello worldHello world
eq / ne{{#if (eq a b)}}boolean equal / not-equal
or / and{{#if (or a b)}}logical or / and
includes{{#if (includes arr val)}}array includes check (multiselect)

Pre-flight detect rules

Declare detects to verify compatibility before inputs are collected or any steps run. Multiple rules are AND-ed. Supported assertions: exists, equals, matches.

detects:
  - file: package.json
    exists: true
  - pkg: next
    exists: true

Full working example

A complete multi-job generator with inputs, detect rules, parallel steps, dependencies, and conditional steps:

workflow.yamlyaml
# workflow.yaml
name: payment/stripe
on: [add]
description: Add Stripe payment processing

detects:
  - file: package.json
    exists: true

dependencies:
  - auth/jwt

conflicts:
  - payment/paddle

provides:
  - payment

inputs:
  secretKey:
    prompt: "Stripe secret key?"
    required: true
    pattern: "^sk_(test|live)_[A-Za-z0-9]+$"

jobs:
  detect:
    steps:
      - uses: xo/detect-pm
        id: pm
        parallel: true
      - uses: xo/pkg-installed
        id: hasNext
        parallel: true
        with:
          pkg: next

  install:
    needs: [detect]
    steps:
      - uses: xo/install-pkg
        with:
          pkg: stripe
      - uses: xo/env
        with:
          file: .env.example
          variables:
            STRIPE_SECRET_KEY: ""
            STRIPE_WEBHOOK_SECRET: ""

  configure:
    needs: [install]
    steps:
      - if: "steps.hasNext.outputs.installed == true"
        uses: xo/copy
        with:
          from: templates/stripe-route.ts
          to: app/api/stripe/route.ts

      - uses: xo/ast-import
        with:
          file: src/app.module.ts
          import: StripeModule
          from: ./stripe/stripe.module

      - run: "{{ steps.pm.outputs.value }} db:push"

Testing locally

Use xo link inside your generator repo to register it by name. From any project, run it with the short name — no path, no --local flag needed. Changes to workflow.yaml or templates are picked up immediately.

terminal
$cd ~/projects/xo-stripe
$xo link
terminal
$cd ~/my-app
$xo add payment/stripe
$xo add payment/stripe --dry-run -i secretKey=sk_test_abc

For a one-off run without linking:

terminal
$xo add ./xo-stripe --local

Publishing to GitHub

Push to any public GitHub repo — no registry account required. Users reference it with the @github/ prefix, or you can register a short name with xo registry add.

terminal
$git init && git add .
$git commit -m "feat: stripe generator"
$git remote add origin git@github.com:my-org/xo-stripe.git
$git push -u origin main
# Direct — no registration needed
xo add @github/my-org/xo-stripe

# Pin to a tag (cached forever)
xo add @github/my-org/xo-stripe@v1.0.0

# Subpath in a multi-generator repo
xo add @github/my-org/xo-generators/stripe
For private repos, users set XO_GITHUB_TOKEN=ghp_xxx in their environment. Branch refs are always re-fetched; tag refs are cached forever.