Building Generators

Signals & Detection

xo has two detection mechanisms: pre-flight detect rules that run before any input is collected, and detection actions (xo/detect-*) that run explicitly inside a workflow's detect job. xo never auto-scans — generators declare exactly what they need to know.

Pre-flight detect rules

Declare detects: in workflow.yaml to verify project compatibility before inputs are collected or any steps run. All rules are AND-ed — every one must pass. If any rule fails, xo refuses to run with a clear error message.

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

Flutter example — only runs in Flutter projects:

detects:
  - file: pubspec.yaml
    exists: true
FieldTypeDescription
filestringRelative path — checks whether the file exists in the project
pkgstringPackage name — checks package.json dependencies
existsbooleanFile/pkg must be present (true) or absent (false). Default: true
equalsstringValue must exactly equal this string
matchesstringValue must match this regex
detects is for simple file/package existence checks only. For richer detection — detecting the package manager, language, or reading JSON values — use detection actions inside a detect job.

Detection actions

Detection actions run inside a workflow's detect job and expose their findings as step outputs. They always run — even in --dry-run mode — because they have no side effects. Assign an id to capture their outputs.

A typical detect job parallelises all checks then gates later jobs on the results:

jobs:
  detect:
    steps:
      - uses: xo/detect-pm
        id: pm
        parallel: true
      - uses: xo/detect-lang
        id: lang
        parallel: true
      - uses: xo/pkg-installed
        id: hasNext
        parallel: true
        with:
          pkg: next
      - uses: xo/file-exists
        id: hasPrisma
        with:
          path: prisma/schema.prisma

  install:
    needs: [detect]
    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"

Detection action reference

xo/detect-pm

Detects the active package manager by checking for lockfiles (pnpm-lock.yaml, yarn.lock, bun.lockb, package-lock.json).

# outputs: { value: "pnpm" | "npm" | "yarn" | "bun" }
- uses: xo/detect-pm
  id: pm

- run: "{{ steps.pm.outputs.value }} install"
xo/detect-lang

Detects the primary language of the project from well-known indicator files (tsconfig.json → typescript, pubspec.yaml → dart, go.mod → go, Cargo.toml → rust, pyproject.toml → python).

# outputs: { value: "typescript" | "javascript" | "dart" | "go" | "rust" | "python" }
- uses: xo/detect-lang
  id: lang

- if: "steps.lang.outputs.value == 'typescript'"
  uses: xo/copy
  with:
    from: templates/config.ts
    to: src/config.ts
xo/file-exists

Checks whether a file or directory exists at a path relative to the project root.

# outputs: { exists: true | false }
- uses: xo/file-exists
  id: hasPrisma
  with:
    path: prisma/schema.prisma

- if: "steps.hasPrisma.outputs.exists == false"
  run: pnpm dlx prisma init
xo/pkg-installed

Checks whether a package is listed in dependencies, devDependencies, or peerDependencies.

# outputs: { installed: true | false, version: "^14.0.0" | null }
- uses: xo/pkg-installed
  id: hasNext
  with:
    pkg: next

- if: "steps.hasNext.outputs.installed == true"
  uses: xo/copy
  with:
    from: templates/next-route.ts
    to: app/api/stripe/route.ts
xo/read-json

Reads a value from a JSON file at a dot-notation path. Useful for reading project name, version, or any config value.

# outputs: { value: <any> }
- uses: xo/read-json
  id: pkgName
  with:
    file: package.json
    path: name

- run: echo "Project: {{ steps.pkgName.outputs.value }}"

Context variables

Any string value in a step's with:, run:, or if:can reference context via {{ double-braces }}:

NamespaceSource
{{ inputs.* }}Answers collected from workflow inputs
{{ steps.<id>.outputs.* }}Outputs from a previous step that has an id
{{ config.* }}Values from xo.config.yaml in the project
{{ env.* }}Environment variables (process.env)
- uses: xo/copy
  with:
    from: templates/component.tsx
    to: "{{ config.ui.componentsDir }}/{{ inputs.componentName }}.tsx"

- run: "{{ steps.pm.outputs.value }} add {{ inputs.pkg }}"

Using detection results in templates

Template files (used with xo/template) have full access to inputs, step outputs, and config via Handlebars. Use the {{#if (includes inputs.features "name")}} helper to conditionally render blocks based on multiselect answers.

{# templates/app.dart #}
import 'package:flutter/material.dart';
{{#if (includes inputs.features "riverpod")}}
import 'package:flutter_riverpod/flutter_riverpod.dart';
{{/if}}

void main() {
  runApp(
    {{#if (includes inputs.features "riverpod")}}
    const ProviderScope(child: MyApp()),
    {{else}}
    const MyApp(),
    {{/if}}
  );
}

See the Actions Reference for the full list of Handlebars helpers available in templates.