Appearance
Skills and the workflow contract
You'll learn how a project's skills and its WORKFLOW.md decide, for every ticket step, exactly one skill that runs it — and the handful of rules that make that safe.
GAIA does not ship a workflow engine you configure. It ships skills — Markdown instruction files the coding agent reads — and each project decides which ones it loads. There is no dispatcher, no central decision table, no config-merge engine and no hidden hook. That is why a project's entire automation surface fits in one short file you can read in a minute.
Two rules carry the model: a skill owns a step, including deciding where the ticket goes next; and WORKFLOW.md only loads skills — it sequences nothing, routes nothing, merges nothing.
A step-owning skill
A step-owning skill declares one when: block in its frontmatter and owns the matched step's whole flow: read the ticket, run the entry guard, provision the environment, frame the run, do the work, validate it, render the outtake, take the confirmation the step requires, choose the transition destination and write it, publish the status to the origin issue, and stop. There is no phases / gate / transitions_to matrix anywhere: the destination is stated in the skill's own prose for that step.
The skills GAIA ships cover qualification, spec, coding, review, deployment and verification. A project loads them, overrides the few values that are project-specific, and adds its own where its stack needs different behaviour.
The when triple
yaml
when:
work_type: code # the ticket's work:* label without the prefix
workflow: [gaia_bug] # a list, a single string, "*", or omitted
step: [diagnose, coding, review] # the workflow states this skill owns
inputs: # each: a dotted role key + description + default
validate.command:
description: the command that runs the test suite
default: ddev e2e
provision.command:
description: the command that brings up the test environment
default: ddev initThe when value | Matches |
|---|---|
| a single string | that value only |
| a list of strings | any member of the list |
* or omitted | any value |
qualification runs before the sub-works are known, so it carries no work type and is matched only by a skill whose work_type is omitted or *. From spec on, the qualification handoff has set the work:* labels, and the work type selects the owner — @gaia/create-spec owns spec for code, @gaia/docs-authoring owns it for docs.
Exactly one match
For the ticket's current (work_type, workflow, step) triple, exactly one loaded skill must match:
- no match is a coverage gap — a configuration error;
- two matches is a collision — a configuration error.
The distributed set of when blocks is the decision table you would otherwise maintain centrally, except each row is declared by the skill that implements it and cannot drift from it. When you add a skill for your own stack you are not extending a table, you are claiming triples; claim one that is taken and the load fails loudly.
WORKFLOW.md — the per-project loader
One load-bearing section, ## Loaded skills, one bullet per skill, optional indented YAML under a bullet to override that skill's inputs:
md
## Loaded skills
- @gaia/essential-skills
- @gaia/ddev-drupal-bug
validate.command: pnpm test:e2e
provision.command: docker compose up -d
- @gaia/docs-authoringThat is the whole loading mechanism. The file's other sections — ## Required skills, ## Domain skills, ## Standards — are read as prose by the steps, not by the loader; the practical guide is Author WORKFLOW.md. Order is meaningful in exactly one case, below.
The section boundary is real, and so is the indentation
The loader reads ## Loaded skills until the next ## heading, and inside that section a list item at indent 0 is an address. A code fence does not shelter one, so an example bullet you put there is parsed as a real load.
Indentation is what separates an address from a value: an indented - @… line belongs to the bullet above it, which is how a .prompt value legitimately lists the skills it requires. And a line at indent 0 that starts - @ but is not - @<namespace>/<skill-name> fails the load with that line quoted back at you, rather than passing quietly as prose.
Inputs — a description, a default, one override site
Everything project-specific a skill needs — how to validate, how to provision, how to build, how to review — is a declared input, never baked into the skill body. Each input is an instruction the agent reads and acts on; the key names its role and its kind (validate.command, provision.command, build.prompt, review.prompt), so a multi-line policy is as valid a value as a one-line command.
Every input has a default, so every skill runs with no project config. To deviate, override that one key under that one skill's bullet. Resolution is a single lookup, narrowest wins:
WORKFLOW.md inline override > bundle inline value > the skill's own inputs.<key>.defaultNo global values block, no inheritance chain, no deep merge.
Bundles
A bundle is a skill that carries a ## Loaded skills section of its own; loading it loads its members. GAIA ships one, @gaia/essential-skills. Expansion is deterministic and idempotent — leaves are deduplicated by name, first occurrence wins — and a name that does not resolve, a cycle, or values written under the bundle's own bullet each raise a typed error. A bundle has no opt-out syntax: to drop or swap a member, write your own bundle and load that instead.
gaia validate — check the load before a ticket hits it
Every rule above is checked by one command. Run it from your project root, or point it at a project directory or a WORKFLOW.md file:
sh
gaia validateIt reads your loader, indexes every SKILL.md it can reach under .claude/skills and .agents/skills, expands the bundles into their members, derives the (work_type, workflow, step) triples your project can produce, and enforces the contract over that set: exactly one owner per triple, and every inline override naming an input the skill really declares.
Green goes to stdout and exits 0:
OK — 9 step owner(s) cover all 24 project triple(s), every override resolves.
owners: qualify-ticket, create-spec, prepare-deployment, post-deployment, verify-on-environment, …A defect goes to stderr and exits 1, one message naming the first thing to fix. The three you will actually meet — a gap, a collision, and an override that resolves to nothing:
FAILED: no skill matches triple (code, gaia_feature, coding) — coverage gap.
FAILED: 2 skills match triple (code, gaia_bug, diagnose) — collision: ddev-drupal-bug, dup-owner.
FAILED: skill `docs-authoring` has no input `validate`: the override resolves to nothing and the
skill silently runs on its default — the skill declares `validate.command`, `review.prompt`,
`design.prompt`, `build.prompt`. Rename the override to a declared key rather than deleting it.That last one is the failure the command exists for. Rename an input key and the override left behind in WORKFLOW.md resolves to nothing: the skill quietly runs its default and says nothing about it. Run this in CI, or before a run, and you meet it the day it appears rather than the week someone notices.
If your project does not vendor its skills, tell the scan where they are. --skills repeats, and takes precedence over the two discovered roots:
sh
gaia validate --skills ~/.claude/plugins/cache/gaia/gaia/0.21.0/skillsThe Claude plugin cache is deliberately not a default root. It holds every installed version side by side, so scanning it blind would report OK against whichever version the walk reached first — the silent pass this command exists to prevent. Name the version you mean.
Helper skills route nothing
The capability skills a step invokes — read the ticket, verify, render the intake and the outtake, transition, publish the origin status — declare no when, produce one observable result each, and never choose a destination. One narrow exception: the qualification guard may route a ticket back to qualification when its handoff is missing, stale or contradictory.
Several sub-works on one ticket
A ticket may carry more than one work:* label. At a given step one skill matches per work type; all of them run in WORKFLOW.md load order, each with its own build-and-validate body, and the last matching skill in load order performs the single confirmation, transition and origin-status write, gated on every sub-work having reached that step's gate.
Standards reach a subagent only by name
WORKFLOW.md also carries, outside the loaded list, a ## Domain skills section — the skills that encode your standards, each with when it applies — and a ## Standards section that points at the sources that bind the repository. The reason is a specific, silent failure mode: a dispatched subagent does not discover skills on its own. When a step hands a build to an isolated subagent, your framework rules and test conventions are simply not applied unless the handoff names them. So every dispatching skill names the applicable domain skills explicitly, from that section, and standards bind from spec onward, not only at coding. A .prompt input that names skills is binding in the same way, under ## Required skills.
Handoffs and ticket text are Markdown in the gaia_rich format — the rule and the trap are on Handoffs & comments.
Next
- Author WORKFLOW.md — the practical path.
- Labels & work types — where the
work_typecomes from. - How GAIA works — where skills sit in the run lifecycle.