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 batch — apex-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 graphPull 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 firstThis 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 pageA 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 outputapply — 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 apply — apply 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
plancan’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.
plannever spends API credits. The only command that talks to a provider ispull.
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 verdictspull 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_thresholdvs 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_maxbut clicked belowk ×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 effect — apply 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 applyThis 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 hubAuthor 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.