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, scaffolded with gtmesh init --template acme) and a Slack keyword pull.

The one idea: the committed graph is desired state

Everything orbits the committed graph (graph/nodes.jsonl + graph/edges.tsv) — 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, manifest), 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 dlx @benfoster/gtmesh init # one-shot from the registry; the mesh then pins its own — see [Installation](/cli/installation/) export AHREFS_API_TOKEN=# required for `pull demand`; never commit it

The steps

Initialize + run the operator

gtmesh init scaffolds a clean mesh repo immediately — no pre-init step. This walkthrough follows the --template acme overlay, which fills the repo with a worked example (the acme-integrations SaaS mesh) so every step below has real content to act on:

mkdir acme-integrations && cd acme-integrations gtmesh init --template acme # overlays the acme-integrations worked example on the generic base # …a plain `gtmesh init` scaffolds a clean, demo-free repo instead: gtmesh init # placeholder config, empty reference CSVs, generic foundations/schemas

gtmesh 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. Run gtmesh init --list-templates to see the available templates.

Then open the repo in Claude Code and run the repo-local operator skill. The operator drives the whole build loop conversationally and demand-first: it interviews you for context, authors foundation/brand.md, validates real keyword demand first (via gtmesh pull demand --keyword/serp, which needs AHREFS_API_TOKEN), builds config and reference tables through the deterministic CLI verbs (gtmesh config set/add, gtmesh reference then authors the CSVs + gtmesh page validate), runs gtmesh plan, resolves the unresolved and cannibalization findings, and gtmesh applys — one slice at a time, with a human approval gate on every judgement call. It keeps the mesh’s working memoryOPERATOR.md plus operator/{decisions,journal,tasks} — so a fresh session can reorient from repo state + gtmesh doctor/gtmesh status.

The steps that follow (pull/plan/apply/promote/write/seal/publish on the acme example) are exactly the verbs the operator runs on your behalf — shown here individually so you can see what each does.

  • Reads (init): the bundled scaffold template.
  • Writes (init): the whole mesh repo (config, reference/, schemas/, templates/, foundation/, .claude/skills/, …).
  • Committed: everything init writes is committed; it is your project.
  • Human gate: the operator seeks approval on every judgement call.

Harvest an entity class (optional)

Some classes — a glossary especially — are not derivable from a keyword pull against your domain axes, 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 (pull)

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

pull 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 pull demand --source csv --input export.csv --topic 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 pull demand --source seeds --dry-run # report the classes + term counts, spend nothing gtmesh pull demand --source seeds # refresh every configured discovery class gtmesh pull demand --source seeds --discovery-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 page is catalogued — it is recorded in the graph, but no page files are created and no built hashes are set.

  • Reads: the same inputs as plan.
  • Writes: nodes/edges into graph/{nodes.jsonl,edges.tsv}.
  • Committed: yes — commit the graph change.

Promote: choose what to build (the gate)

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

This is the gate between the two loops. Promotion is intent-based: it selects pages by field predicates 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, --has, --topic, --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 committed graph.
  • Writes: the status of matched pages.
  • Human gate: this is the gate — you decide what gets built.

Scaffold the promoted pages (apply)

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

Now apply acts on the promoted pages: 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 committed graph, 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 and foundation/* (where brand.md carries the strategic “why”);
  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 page validate <slug>;
  4. invokes the independent review-gate skill (fail → revise in place, stays writing; pass → proceed);
  5. on pass, hands the slug back for you to seal — the page stays in writing.

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, and foundation (incl. the strategic “why” in brand.md).
  • Writes: the page’s content and editorial meta fields — inside that page’s bundle, nothing else. So you can run several writers on different slugs at once.
  • Human gate: you review the drafts, then seal the batch (gtmesh page seal <slug…>).

Seal: validate and record (seal)

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

gtmesh page 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_brief_hash / built_schema_hash / body_hash — the provenance that makes later diffs precise.

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

Publish (publish)

gtmesh page 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 page amend <slug> # body changed (assets placed) → needs_update (the skill runs this) gtmesh page seal <slug> # re-validates with assets present, re-stamps body_hash → review gtmesh page 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 committed graph 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, redirect, a zero-rewrite manifest re-projection, …) is the lifecycle reference. For the full command surface, see the CLI reference.

Last updated on