Skip to Content
DocumentationCreate a meshEnd-to-end walkthrough

End-to-end walkthrough

This is the whole journey from an empty directory to a live page, following the bundled acme-integrations worked example (a fictional SaaS that connects to third-party tools) and a Slack keyword pull.

The one idea: the registry is desired state

Everything orbits the committed registry, registry/pages.csv — the source of truth for every page that could exist and what state each is in. You never hand-edit it. Instead you change an input (a keyword pull, a config rule, a page’s status) and then run apply, which makes reality match. apply is idempotent and Terraform-style: it previews what it will do and prompts before writing, so it is always safe to re-run. If a term below is unfamiliar (catalogue, seal, restamp), see the glossary.

plan is optional. apply computes the same diff itself. Run plan whenever you want a read-only preview; you never need it between a change and an apply.

Prerequisites

pnpm add -g gtmesh # (or, from source: pnpm install && pnpm build && pnpm link --global) export AHREFS_API_TOKEN=# required for `extract --source ahrefs`; never commit it

The steps

Discover the project shape (optional)

To start tailored to your business instead of the generic demo, run project discovery first. Make an empty directory and run:

mkdir mybrand && cd mybrand gtmesh discover "what you sell/publish, audience, market, URL" # → installs the `discovery` skill into .claude/skills/ and prints a prompt

gtmesh discover is LLM-free — it just installs the skill and hands you a prompt. Open the directory in Claude Code and paste the prompt. The discovery skill interviews you, pulls a demand-validated information architecture (sections, clusters, the page types it needs, harvest classes, clustered starter terms), and writes gtmesh-discovery.yaml, then stops.

  • Reads: your answers in the interview; live demand signals.
  • Writes: gtmesh-discovery.yaml (you review and edit it).
  • Human gate: you review the discovery file before step 1.

Skip this step to start from the generic Acme demo instead. The contract for that file is documented in the discovery contract.

Scaffold the repo (init)

mkdir acme-integrations && cd acme-integrations gtmesh init # writes the documented mesh repo (config, reference, schemas, skills…) # …or, tailored from step 0: gtmesh init --from-discovery gtmesh-discovery.yaml

init writes a complete, documented mesh repo — the structural-law config, the reference tables, the page schemas, the templates, the foundation/voice files, and the repo-local skills. These files are yours to tune to your domain.

With --from-discovery, init scaffolds the full library, then overlays the discovery shape onto gtmesh.config.yaml (preserving the educational comments): your project, taxonomy, section routing, the selected page types, and the harvest classes; it writes each cluster’s starter terms to seeds/<class>.csv, and templates your project identity into the README. It also clears the demo’s worked-example data so a tailored mesh does not inherit Acme’s. The full overlay rules are in the discovery contract.

  • Reads: the bundled scaffold template; optionally gtmesh-discovery.yaml.
  • Writes: the whole mesh repo (config, reference/, schemas/, templates/, foundation/, .claude/skills/, …).
  • Committed: everything init writes is committed; it is your project.

Harvest an entity class (optional)

Some classes — a glossary especially — are not derivable from a keyword pull against your clusters, because their terms are a cross-cutting taxonomy rather than children of any one hub. The engine-owned harvest skill figures out what pages should fill a class: it brainstorms candidates, ranks them against a demand corpus, validates demand via Ahrefs, scores the cut, writes a curated term list, and then stops for your review.

gtmesh harvest # lists the configured discovery classes gtmesh harvest glossary # prints the prompt to run in Claude Code (+ the next steps)

gtmesh harvest is the CLI-to-skill bridge: it validates the class and hands you a prompt. In Claude Code you run that prompt (e.g. harvest glossary), which writes seeds/glossary.csv (term,target_keyword,family,section,page_type,sourcenever metrics). Re-running only proposes new terms, so review stays small.

  • Reads: the discovery: class config; the demand corpus CSV.
  • Writes: seeds/<class>.csv (terms + provenance only).
  • Human gate: you review and edit the term list — curation is the point.

The term list is the durable, human-reviewed artifact. Volume and difficulty live in data/raw/ and refresh on cadence — never in the seed file.

Extract keyword data (extract)

gtmesh extract --cluster slack --dry-run # report the planned pulls, spend no credits gtmesh extract --cluster slack # two pulls (matching-terms + questions) → data/raw/keywords/ahrefs/ gtmesh extract # all configured seed clusters

extract pulls keyword/search signals into the data bag. Raw exports are immutable and timestamped; plan reads the frozen bag and never re-fetches. No API budget? gtmesh extract --source csv --input export.csv --cluster slack imports a CSV instead.

If you curated a term list with harvest, refresh its volume/difficulty without expanding it via the overview endpoint:

gtmesh extract --source seeds --dry-run # report the classes + term counts, spend nothing gtmesh extract --source seeds # refresh every configured discovery class gtmesh extract --source seeds --class glossary # just one class
  • Reads: the adapters.ahrefs config and your seeds/clusters.
  • Writes: timestamped exports under data/raw/keywords/ahrefs/.
  • Committed: yes — the raw exports are diffable and committed.

Preview the diff (plan, optional)

gtmesh plan # prints the plan to stdout (like `terraform plan`); also writes .gtmesh/plan.json gtmesh plan > plan.md # pipe stdout to a file if you want to keep the rendered plan gtmesh plan --json | jq . # machine output instead

The printed plan shows the action summary, the catalogue/create breakdown by page type, anything needing human review, and an Unresolved list — pages that matched no sections_map/page_types rule. The engine never guesses: fix the config rule (or add a seed/override) and re-plan until the unresolved list is what you expect. Re-planning is free and changes nothing.

  • Reads: the config, reference tables, seeds, and the data bag.
  • Writes: the human view to stdout; a derived .gtmesh/plan.json (git-ignored).

Catalogue everything (apply)

gtmesh apply # shows the intended actions, prompts to confirm, then enacts gtmesh apply --yes # skip the prompt (scripts/CI; required in a non-interactive shell)

apply prints what it will do (the same diff as plan) and prompts Proceed? [y/N] before writing. On this first run every row is catalogued — it is recorded in the registry, but no page files are created and no built hashes are set.

  • Reads: the same inputs as plan.
  • Writes: rows into registry/pages.csv.
  • Committed: yes — commit the registry change.

Promote: choose what to build (the gate)

gtmesh promote --section glossary --dry-run # preview: which rows would move planned → queued gtmesh promote --section glossary # commit the promotion

This is the gate between the two loops. Promotion is intent-based: it selects rows by registry fields or by the mesh graph, writes only their status, and builds nothing. It is reversible with demote, and applies by default (--dry-run previews).

Selectors compose (with AND), e.g. --section, --cluster, --tier, --page-type, --slug-prefix, or positional exact slugs; --under <hub-slug> promotes a hub and everything reachable down the mesh from it. The full selector table is in the lifecycle reference.

  • Reads: the registry.
  • Writes: the status column of matched rows.
  • Human gate: this is the gate — you decide what gets built.

Scaffold the promoted rows (apply)

gtmesh apply # queued rows → content/<slug>/index.yaml (schema skeleton + _brief), status → writing

Now apply acts on the promoted rows: it scaffolds each as a bundle folder, content/<slug>/index.yaml, holding the schema skeleton plus an engine-owned _brief block (the writer’s assignment). Only slug is deterministic; the editorial meta fields (metaTitle/title/navTitle/metaDescription) are scaffolded empty for the writer to fill. Mesh links are not on the page — they live in the registry, so there is no second copy to drift.

  • Reads: the registry and the page schemas.
  • Writes: content/<slug>/index.yaml bundles; flips status to writing.
  • Committed: yes.

Write the body (the article-writer skill)

gtmesh status # the worklist: pages in `writing` await a body (status --json → .writing)

This is the only step that uses a model. Invoke the repo-local article-writer skill. Per page it:

  1. reads only that page’s _brief and schema stubs, plus templates/<type>.md, foundation/*, and strategy.md;
  2. fills the content fields, and for any image slot authors an art-direction brief (artDirection, no asset:) — it does not generate images (that is step 10);
  3. runs the humanizer skill, then self-checks with gtmesh validate <slug>;
  4. invokes the independent review-gate skill (fail → revise in place, stays writing; pass → proceed);
  5. on pass, prompts you: review the draft, or auto-seal?

The CLI and the writer communicate only through the page file (_brief + content) and the registry status. The CLI never calls a model; the writer never touches frontmatter or the registry.

  • Reads: the page _brief, schema stubs, templates, foundation, strategy.
  • Writes: the page’s content and editorial meta fields.
  • Human gate: you review the draft (or opt into auto-seal).

Seal: validate and record (seal)

gtmesh validate # also checks committed INPUTS: reference tables, seed-pages, discovery seeds gtmesh validate <slug> # schema (ajv) + deterministic editorial lint; the writer's self-check gtmesh seal <slug> # only if schema-valid AND lint-clean: → review, stamps the 3 built hashes

gtmesh validate first checks your committed input CSVs against the engine’s column contracts, so a malformed input is caught at the source. A page failing schema or an editorial lint cannot seal. seal moves the page to review and stamps built_content_hash / built_projection_hash / built_schema_hash — the provenance that makes later diffs precise.

  • Reads: the page, its schema, the editorial rules.
  • Writes: status → review; the three built hashes in the registry.

Publish (publish)

gtmesh publish <slug> # after the human PR review gtmesh status --env prod # what's live; render-manifest --env prod for the renderable slug set

After your PR review, publish moves the sealed page to published. A page with only image briefs (no asset: yet) publishes fine — the briefs are schema-valid and the SSG renders placeholders.

  • Reads: the registry.
  • Writes: status → published.
  • Human gate: the PR review precedes publish.

Images: generate and place (the image-director skill)

Image generation is networked and non-deterministic, so it lives in a skill, never in the CLI. The article-writer emits art-direction briefs (step 7); the explicit image-director skill turns them into placed files. Invoke it on a slug — it reads gtmesh.config.yaml’s images: block and foundation/art-direction.md, generates via the fal.ai MCP, writes the asset files into the bundle, and adds asset: to each slot. Placing assets is a body edit, so the page re-enters a light re-seal:

gtmesh amend <slug> # body changed (assets placed) → needs_update (the skill runs this) gtmesh seal <slug> # re-validates with assets present, re-stamps body_hash → review gtmesh publish <slug> # → published, now with images

Both orderings work: publish copy first and add images later (each pass is an amend → seal → publish re-entry), or generate before the first publish (one pass, no re-entry, but slower to first publish).

  • Reads: the page’s art-direction briefs; images: config; art-direction foundation.
  • Writes: asset files into the bundle; asset: refs on each slot.
  • Human gate: the re-seal review before re-publishing.

After the bootstrap

Once the registry exists you don’t bootstrap again — you change an input and reconcile. That steady state is the refresh loop, and exactly what each kind of change does to a built page (rewrite, restamp, redirect, prune, …) is the lifecycle reference. For the full command surface, see the CLI reference.

Last updated on