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: trueFlutter example — only runs in Flutter projects:
detects:
- file: pubspec.yaml
exists: true| Field | Type | Description |
|---|---|---|
| file | string | Relative path — checks whether the file exists in the project |
| pkg | string | Package name — checks package.json dependencies |
| exists | boolean | File/pkg must be present (true) or absent (false). Default: true |
| equals | string | Value must exactly equal this string |
| matches | string | Value 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-pmDetects 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-langDetects 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.tsxo/file-existsChecks 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 initxo/pkg-installedChecks 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.tsxo/read-jsonReads 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 }}:
| Namespace | Source |
|---|---|
| {{ 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.