Skip to Content

The refresh loop

Once the committed graph exists you don’t bootstrap again — you change an input and reconcile. In steady state that’s a short rhythm — one command does the pull-and-preview:

gtmesh topic refresh slack # pull → plan → stop at the apply gate (omit slack for every root) gtmesh apply # enact it after review (or `topic refresh --apply` to bundle) gtmesh topic status # per topic, demand vs coverage → the concrete next action gtmesh next # which un-built pages to build this cycle (paced)

gtmesh topic refresh is the reconcile pull (pull → plan) in one command, stopping before it writes; topic status closes the loop by telling you what each topic needs. The rest of this page explains why each step behaves the way it does — and why you can run the cheap part on a schedule without worrying.

doctor is your topic to-do list. After a refresh it evaluates each topic’s coverage and tells you what to do next — so you don’t have to guess. root-topic-uncovered names a root you declared a pull frontier but haven’t built a page for (author it with gtmesh topic add --apex, or topic unroot it); its sibling question-content-gap flags demand-worthy questions with no content page to answer them. Both are ranked so you act on the valuable gaps first. And pull pulls the full demand (matching-terms and questions) per root by default, so a single refresh never leaves you re-pulling for questions later.

The loop, defined

The topic-first refresh is a fixed, self-explaining cadence — no guessing what to run:

gtmesh topic refresh # ← DO A REFRESH: pull → plan → stop at the apply gate gtmesh apply # enact it (review the plan first; or `topic refresh --apply` to bundle) gtmesh topic status # ← the oracle: per topic, demand vs coverage → the concrete NEXT ACTION gtmesh next # ← which un-built pages to build THIS cycle (apex-first, paced)

gtmesh topic refresh [<value>] is the whole pull in one command. It runs pull (pull every root topic’s demand — matching-terms + questions — or just <value>), then plan (preview the diff), and stops at the apply gate so you review before it writes. Then gtmesh apply (or topic refresh --apply to enact in the same step). --dry-run re-plans against the bag you already pulled (no Ahrefs credits). It adds no new behaviour — it’s the exact pull → plan → apply sequence with one name — so you never have to remember the order. (The raw verbs still work if you want to run a step at a time.)

gtmesh topic status closes the loop: it tells you what to do next per topic — refresh (a root with no demand yet), author (demand, no page), build the cluster (a hub with no members), ✓ covered, pull-to-assess (pages exist, but its demand was never pulled), or should-root (a demand-bearing hub that isn’t a pull frontier). Work it opportunity-first.

gtmesh next turns that into a concrete build queue. topic status says which topics need work; next ranks the actual un-built pages across all topics into a paced batchapex-first (a topic’s home before its cluster, so authority has somewhere to pool), highest-opportunity-topic-first, page-priority within a tier. It prints the batch + a ready-to-run gtmesh page promote … --to queued (or gtmesh next --promote to actuate); then gtmesh apply scaffolds them for the writer. Building ~10 a cycle (the default --limit) is the gradual growth Google prefers — run next each cycle to pull the next batch.

Deciding where to invest — gtmesh topic candidates. topic status/audit tell you what each topic is missing; candidates tells you which ones are worth the effort. It’s the only surface that ranks by opportunity × winnability (the KD-inverse) rather than opportunity alone — so a huge topic behind a difficulty wall stops outranking a smaller one you could actually own — and gives each a verdict with its command: build (no apex) · promote (built but not a root) · invest (grow the cluster) · hold (a KD wall, once you set doctor.min_winnability). The commercial half of commercial ∩ winnable already rides inside opportunity (total_vol × cpc). Run it when you’re choosing what to take on — before next, which assumes the choice is made and just paces the build. Read the ranking with Winning a topic in mind: a hard head is a horizon, not a veto — check its uncovered tail with topic fill before you skip it.

Check the foundation any time — gtmesh topic audit. A read-only, holistic report of where every topic stands: promote to roots (demand-bearing hubs not being refreshed), pull to assess (built topics whose demand was never pulled, so nothing can rank them yet), build or drop (roots with no page), pull (roots with no demand) — each with its command. Run it on a fresh build, on a schedule to catch drift, or — its most dramatic use — as the first step on an older page-first mesh, where many hubs won’t be roots yet. Set doctor.min_root_opportunity so only the valuable hubs surface; walk the promote list (gtmesh topic add <value> --root --seed "<head>"), then pull to assess — root + gtmesh topic refresh those (biggest cluster first) so they can be ranked on demand next pass. (topic status shows the same should-root and pull-to-assess signals per topic.)

The three commands

pull — refresh the data

gtmesh pull demand pulls newer keyword/search data and writes it to the bag (data/raw/). Each export is immutable and timestamped — pull never overwrites an old pull, it adds a newer one beside it. So the bag is a growing pile of dated snapshots, not a single mutable file.

gtmesh pull demand --topic slack --dry-run # report the planned pulls, spend no credits gtmesh pull demand --topic slack # the real pull → data/raw/keywords/ahrefs/ gtmesh pull demand # every root topic in the graph

Pull targets live in the graph

With no --topic, pull pulls every root topic in the committed graph. A root topic is a node that owns its pull config — its seed_terms (the pull heads) and per-topic exclusions (noise rules layered onto the global excludes for that pull). You author one directly in the graph:

gtmesh topic add submersible-pump --root --seed "submersible pump" --seed "sub pump" gtmesh topic list --roots-only # the pull targets, highest-demand first

This is why “add a new product area” is a one-line graph edit, not a config edit: topic add --root and the next gtmesh pull demand picks it up. Listing is demand-ordered, so when you have credits for only a few pulls you refresh the highest-demand topics first.

Coming from adapters.ahrefs.seeds? gtmesh upgrade moves each seed list onto the topic it names and reports what it moved. A seed key that names no topic in your graph is left in the config and called out by name — give that one its topic yourself (gtmesh topic add <value> --root --seed "<term>") and delete the entry, which is also what clears doctor’s dead-config-key warning. See topic in the CLI reference.

Exploring demand, not just refreshing it? Add --stage and the pull lands in the git-ignored .gtmesh/staging/ instead of the bag — plan ignores it. Then gtmesh stage admit --keyword "<keepers>" promotes only the terms you keep into the bag, without a second pull. This is how you shape the config against real demand without flooding the graph. See Discovery & harvest.

Refresh question demand — enrich existing pages

Refreshing isn’t only about new pages. The questions pull is how your existing built pages gain demand-validated FAQ coverage over time:

gtmesh pull demand --pull questions --topic <id> --stage # long-tail questions → staging gtmesh stage admit --keyword "<keeper>" # curate — raw question demand is noisy gtmesh plan # sub-threshold questions FOLD onto their topic's page

A sub-threshold question (below classification.question_page_min_volume) doesn’t mint a thin page — it folds as an answer block onto the page that owns its topic (an existing glossary/guide, matched by a shared topic or keyword). So the same pull → stage admit → plan → apply rhythm, run for questions, routes real questions onto the pages that should answer them. gtmesh doctor’s question-content-gap check flags a topic that has question demand but no content page to answer it — author one (gtmesh page add) and the next plan routes the questions to it. See folding demand into a hub for the full mint-vs-fold + routing model. (Questions are long-tail, so keep adapters.ahrefs.pull_overrides.questions.volume_floor low or the pull returns little.)

plan — preview the diff

gtmesh plan reads the frozen bag and computes what the committed graph should become. It picks the newest snapshot for each keyword (this is the “recency” behaviour — your latest pull wins) and shows you the diff against the committed graph.

plan is read-only and free. It prints to stdout and writes a derived, git-ignored .gtmesh/plan.json — it never re-fetches data and never changes the graph. Run it whenever you want a look; re-run it as many times as you like.

gtmesh plan # prints the plan to stdout gtmesh plan > plan.md # keep the rendered plan in a file gtmesh plan --json | jq . # machine output

apply — enact it

gtmesh apply makes the committed graph (and the page files) match what plan showed. It’s idempotent: it only acts where there’s a real difference, so running it twice in a row does nothing the second time. Terraform-style, it previews what it will do and prompts to confirm.

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)

You never need plan between an pull and an applyapply computes the same diff itself. plan is the optional preview; apply is the one that writes.

Why the bag is frozen and timestamped

Because pull appends dated snapshots and plan reads the frozen bag, the loop is deterministic and auditable:

  • A given bag always produces the same plan — re-running plan can’t surprise you with different numbers, because the data it reads doesn’t move.
  • The keyword history is preserved. You can see how volume or difficulty drifted over time, and old pulls are never silently lost.
  • plan never spends API credits. The only command that talks to a provider is pull.

Re-pull cadence

How often you re-pull is your call — keyword metrics drift slowly, so most operators pull on a schedule (weekly or monthly) rather than constantly. The key safety property: the catalogue loop is safe to run on a schedule. pull → plan → apply keeps the map of everything that could exist in sync, and because apply only acts on real differences, an automated nightly or weekly run is harmless when nothing changed.

What it is not is an automatic content factory. Re-pulling and applying keeps the catalogue current; it never writes or rewrites a page body on its own. Which catalogued pages you actually build stays a deliberate, human-gated choice — see lifecycle & reconcile.

The loop is two-sided: what to build, and what’s working

Everything above is the demand side — it tells you what’s worth building. Once pages are live, the performance side tells you what’s working, from your own Search Console data:

gtmesh pull performance --source gsc # pull a Search Console snapshot into the committed bag gtmesh doctor # cadence findings: which channel is stale, what the fresh data flags gtmesh topic performance # the ranked demand × performance gap report gtmesh topic performance <value> # deep-dive one topic, page by page gtmesh next --tune # the cycle's build order, boosted by the observed verdicts

pull performance --source gsc writes a timestamped snapshot (the trailing window and the one before it) to data/raw/performance/. It checks its own prerequisites — the first run errors with the exact service-account setup walkthrough if adapters.gsc.property or the key file (gsc-key.json at the mesh root by convention; GSC_KEY_FILE overrides) is missing, so just run it and follow the message. The same pull now also sweeps indexing — every published page’s URL through Google’s URL Inspection API, committed beside the analytics files — so one pull answers “is it working?” and “is it even indexed?”. topic performance then joins the latest snapshot onto the committed topical edges and classifies every topic against your thresholds: config, worst-first:

  • decaying — the cluster’s position dropped ≥ decay_threshold vs the prior window. Improve the apex before anything else here loses more ground.
  • striking-distance — the head keyword sits in [low, high] with impressions above the floor: page one is in reach, so strengthening this apex and its cluster is the highest-leverage work.
  • ctr-gap — ranking inside pos_max but clicked below k × your site’s own CTR curve at that position: a title/snippet problem, not a ranking problem.
  • head-unranked — the cluster earns real impressions (≥ the striking-distance floor) but the head term has no ranking row at all: the topic works, the prize keyword doesn’t yet — point the apex at the head.
  • no-data — the topic has pages but nothing in the snapshot matched them: recently published (give it a window) or the snapshot is stale — re-run the pull.
  • unbuilt — demand with no pages at all; the report prints the gtmesh topic add … to fix it.
  • served — ranking with healthy CTR. Leave it alone.

Each row carries a why (the evidence) and a fix (the next command). Read both sides before deciding where effort goes next: topic status / topic candidates (demand) name what to build; topic performance names what to improve — effort goes where the gap is.

The tune lap — closing the loop

gtmesh next --tune is where the two sides meet: the same paced, apex-first build queue, with pages of actionable-verdict topics boosted within their status tiers — decaying first, then apex-unbuilt, striking-distance, ctr-gap, head-unranked. The structure never bends: a backlog page still never outranks a planned one, a topic’s home still comes before its cluster, and the pacing cap holds. Every boosted page’s why line names the signal (”… is decaying — down 4.4 places since the last window”), so the recommendation stays explainable — and next still only recommends; --promote remains the sole actuator.

So the full cadence, closed: gtmesh pull performance refreshes the observation (pull links / pull ai on their own slower cadence) → gtmesh doctor routes the lap (its cadence findings name the stale channel and the pull verb; its signal findings name what the fresh data flags) → gtmesh topic performance for the verdicts’ detail → gtmesh next --tune for the cycle’s build order → you pick; the verbs act.

Tuning is opt-in: set next.tune: true (gtmesh config set next.tune true) to make it every run’s default, and --no-tune reverts a single run to demand-only ranking. With no performance snapshot yet, next --tune says so in one line and ranks on demand alone.

Performance is a read-time join, not graph state: the snapshot lives in the bag, the report joins it against folded demand on the fly, and plan/apply never read it. Nothing about how your pages perform ever changes what the engine builds — only you do that, informed by the report. See the two data planes.

The same read lives visually in gtmesh ui: the Dashboard tells the site-wide story (visits, rankings, trends, where to focus — without opening Search Console), and the Topics tab tells each topic’s — the same snapshot and verdicts as topic performance, translated into plain language. The Pages tab is where the per-page truth lives — every page’s status, indexing state, and what people searched to find it. Keep the terminal for the cadence; open the UI when you want to see it.

Editing an input shows recompute

When you edit a reference/ table, a seed list, or a config rule, the catalogued-but-unbuilt pages your edit touches show up in the plan as recompute (with reason: inputs-changed). That’s the signal your edit took effectapply will recompute those pages.

This is deliberate. Without it, an edit to an unbuilt page used to read as noop, which made it look like your change did nothing. recompute tells you the difference was registered and is waiting to be applied.

For exactly what every kind of edit triggers on every kind of page — and who acts on it — see lifecycle & reconcile.

Add a new category

To stand up a new cluster — a new category, manufacturer, or any open-axis grouping — author the topic and its apex together, then let apply form the cluster. There is no graph link surgery.

# 1. Author the topic + its apex, born together. --apex mints the hub page; --head becomes its # primary_keyword (= the topic head); --kind is the entity's domain kind (sets the topic axis). gtmesh topic add multistage-pump \ --head "multistage pump" \ --apex category-hub \ --section categories \ --kind category \ --as hub # 2. Build it. This mints topics/multistage-pump and the HUB_OF edge, and routes existing pages. gtmesh apply

This is the topic-first shorthand for gtmesh page add --role hub --parent-topic multistage-pump …, which still works and does the same thing — topic add --apex just lets you name the topic (and derives the apex page’s slug + keyword) instead of specifying the page. Add --root --seed "…" to also make the new topic a pull frontier.

Now add spokes. A spoke joins the cluster with --parent-topic (no --role) — it becomes a member and routes up to the hub:

gtmesh page add \ --slug /glossary/what-is-a-multistage-pump \ --keyword "what is a multistage pump" \ --section glossary \ --type educational \ --parent-topic multistage-pump gtmesh apply # binds it: MEMBER_OF topics/multistage-pump, up-links to the hub

Author the hub first. --parent-topic always ensures the topics/<t> node exists — a spoke authored with --parent-topic <t> does create the topic (and joins it) even if no hub exists yet. But a spoke can’t head a cluster: with no hub or pillar on the topic there is no apex to route up to, so the spoke stays an unrouted member until you author the hub. Hub-first means spokes both bind and route up in one step. (A hub-owned topic is a derived domain cluster the engine regenerates from ownership each apply; a spoke-seeded topic with no hub is an authored node that persists but pools no authority until an apex claims it.)

This is for an open-axis cluster backed by an entity (a category, a manufacturer). A curated thematic topic — a pillar pooling pages that share no entity — is authored differently; see curated topics.

On an existing mesh? The page add flags (--role, --kind, --label, --prop) ship with the CLI — bump the gtmesh binary to get them. Then run gtmesh upgrade to refresh the operator skill with this hub-first flow. Both are additive — nothing breaks if you do neither.

Last updated on