| name | flow-package-format |
| description | The §7 Flow Package format — directory layout, package.json shape (including the `relay` metadata block), entry point contract (default-export from flow.ts), README §7.4 ordered sections, and the semver discipline that catalog flows follow. Trigger this skill when creating or validating a flow package — examples in `examples/`, reference flows in `packages/flows/`, generator templates in `packages/generator/templates/`, the lint check in `packages/cli/src/lint.ts`, and the registry generator in `packages/cli/src/registry.ts`. |
Flow Package Format
A flow is a self-contained directory that can be installed, versioned, forked, and run independently. Every flow — whether installed from the catalog, generated by the skill, or hand-written — has the same shape.
Directory layout (§7.1 — canonical)
<flow-name>/
├── package.json # name, version, deps on @ganderbite/relay-core
├── flow.ts # the defineFlow() entry point — default export
├── prompts/
│ ├── 01_<step>.md
│ ├── 02_<step>.md
│ └── ...
├── schemas/ # optional: shared Zod schemas
│ └── <name>.ts
├── templates/ # optional: output templates (HTML, markdown)
│ └── <name>.<ext>.ejs
├── examples/ # optional: sample outputs for the README
│ └── sample-output.<ext>
├── README.md # user-facing docs (see §7.4)
└── tsconfig.json # extends @ganderbite/relay-core/tsconfig
package.json (§7.2 — required fields)
{
"name": "@ganderbite/relay-codebase-discovery",
"version": "0.1.0",
"description": "...",
"type": "module",
"main": "./dist/flow.js",
"files": ["dist", "prompts", "schemas", "templates", "examples", "README.md"],
"scripts": {
"build": "tsc",
"test": "relay test ."
},
"peerDependencies": {
"@ganderbite/relay-core": "^1.0.0"
},
"relay": {
"displayName": "Codebase Discovery",
"tags": ["discovery", "documentation"],
"estimatedCostUsd": { "min": 0.20, "max": 0.80 },
"estimatedDurationMin": { "min": 5, "max": 20 },
"audience": ["pm", "dev"]
}
}
Naming convention (per product spec §3.4): Catalog flow packages are named @ganderbite/relay-<name> — note relay- prefix, not flow-. The CLI's relay install command resolves bare <name> to @ganderbite/relay-<name>.
The relay metadata block is consumed by the CLI's list, search, pre-run banner, and the catalog site's per-flow page. Required keys: displayName, tags, estimatedCostUsd { min, max }, estimatedDurationMin { min, max }, audience. The library itself doesn't read this block — it's purely metadata.
flow.ts entry point (§7.3)
import { defineFlow, step, z } from '@ganderbite/relay-core';
export default defineFlow({
name: 'codebase-discovery',
version: '0.1.0',
description: 'Produces an HTML codebase report for PMs and devs.',
input: z.object({
repoPath: z.string(),
audience: z.enum(['pm', 'dev', 'both']).default('both'),
}),
steps: {
inventory: step.prompt({ }),
entities: step.prompt({ dependsOn: ['inventory'], }),
services: step.prompt({ dependsOn: ['inventory'], }),
report: step.prompt({ dependsOn: ['entities', 'services'], }),
},
});
Rules:
flow.ts MUST default-export a Flow object built by defineFlow.
- The CLI loads it via dynamic ESM
import() of the compiled dist/flow.js.
- Flows are compiled to JS at publish time — the CLI never runs
tsc on install.
- The flow's
name should match the package's bare name (codebase-discovery).
- The flow's
version is independent from the package's version, but typically tracks it.
Script steps — env declarations and auto-injected vars
A step.script({ run, env, ... }) step runs a shell command in a child process. Its env map accepts entries in two forms — collectively typed ScriptEnvValueSpec:
step.script({
env: {
BUILD_LABEL: '{{input.repo}}-{{plan.title}}',
REPO: { from: 'input.repo', required: true },
TITLE: { from: 'handoff.plan.title' },
PATH: { from: 'input.outDir', resolve: 'fromCwd' },
},
run: ['bash', 'scripts/build.sh'],
});
ScriptEnvFromSpec fields:
| Field | Meaning |
|---|
from | Required. Either input.<dot.path> or handoff.<step>.<dot.path>. |
required | If true, an undefined or null resolution aborts the step with a FlowDefinitionError. Default false — missing values render as the empty string. |
resolve | Either 'fromCwd' (resolve as a path against process.cwd()) or 'absolute' (treat as already absolute). Default unset — value is used verbatim. |
RELAY_ auto-injected vars.* Every script step receives these four environment variables, set by the runtime before the user-supplied env map is merged in:
| Var | Value |
|---|
RELAY_RUN_DIR | The current run's working directory (<flowDir>/.relay/runs/<runId>/). |
RELAY_FLOW_DIR | The flow package directory (where flow.ts lives). |
RELAY_HANDOFFS_DIR | The handoffs subdirectory (<runDir>/handoffs/). |
RELAY_INPUT_JSON | Path to a JSON file containing the flow's parsed input — read this instead of re-parsing argv. |
Script steps that need structured input should cat "$RELAY_INPUT_JSON" and parse it; steps that emit artifacts should write under $RELAY_RUN_DIR.
README.md (§7.4 — ordered sections)
Every flow's README MUST contain these sections, in this order:
- What it does — one paragraph.
- Sample output — image or excerpt.
- Estimated cost and duration.
- Install command —
relay install <name>.
- Run command —
relay run <name> <input> with most common arguments.
- Configuration — what knobs the flow exposes.
- Customization guide — how to fork and adapt.
- License.
Sections 1–5 are mandatory (linter ERROR if missing). Sections 6–8 are recommended (linter WARN if missing). The catalog homepage rejects flows that lack 1–5.
See references/readme-template.md for a fillable template.
Semver (§7.5)
Every flow follows strict semver:
| Change | Bump |
|---|
Breaking change to input schema | Major |
| Removal of an artifact | Major |
| Rename of a public output (handoff/artifact) | Major |
| Adding optional inputs | Minor |
| Adding new artifacts | Minor |
| Prompt tweaks, bug fixes | Patch |
The catalog client respects npm semver: relay install codebase-discovery@^0.1.0 is honored.
Resolution order (§5.3.1 — for relay install and relay run)
The CLI resolves a flow name in this order:
- Local:
./.relay/flows/<name>/
- Workspace package:
./node_modules/@ganderbite/relay-<name>/
- Remote catalog:
https://ganderbite.github.io/relay/registry.json → fetch tarball
A locally installed flow always wins. The CLI prints which source it resolved.
Local mode
If the first positional looks like a path (starts with ./, ../, /, or contains /), the CLI treats it as a local flow directory, skipping resolution. This is the development mode for flow authors.
Validation
The lintFlowPackage(dir) function checks:
package.json exists with all required fields.
flow.ts OR dist/flow.js is loadable and default-exports a Flow (duck-typed: has name, steps, graph).
README.md contains the §7.4 ordered headings (grep for the heading strings) — missing 1–5 is ERROR, missing 6–8 is WARN.
prompts/ directory exists if any step.prompt references promptFile.
schemas/*.ts files compile cleanly.
Returns { errors: LintIssue[]; warnings: LintIssue[] }.
See references/directory-layout.md for the canonical tree, references/package-json.md for the full schema, and references/readme-template.md for the README skeleton.