Deploys & drift
Once Email Guard is live, HS-X keeps three separate records about it, and the dashboard vocabulary — deploys, drift, checkpoints — is just those three records by name. A deploy revision is the signed receipt of what you shipped. Drift is a recurring verdict on whether what is actually running still matches that receipt. Checkpoints are the per-invocation telemetry of what the running app is doing. This guide walks one deploy through its whole lifecycle, then shows you how to read each record from the CLI and the dashboard — including the cases that look wrong but aren't, like an installable, fully live app that still reads 'no recorded deploys'.
TL;DR — hs-x deploy ships code and records a revision: deploy id, manifest hash, and the captured Cloudflare Worker version a rollback can restore traffic to. The runtime then attests on the first request each Worker isolate serves and at most once per 15 minutes after that; the control plane diffs the attestation against the recorded revision and publishes the verdict as drift (advisory — your app keeps serving either way). Checkpoints are the separate per-invocation stream (outcomes, latency, error fingerprints) behind the dashboard’s metrics. Deploys made while unlinked are recorded locally and backfilled into your history when you hs-x link.
The three records HS-X keeps about your app
Every operational question about a deployed project reduces to one of three questions, and each has its own record:
| Question | Record | Where it lives | Who writes it |
|---|---|---|---|
| What did I ship? | Deploy revision — deploy id, signed manifest hash, captured Cloudflare Worker version, git provenance | Control plane (Deploys tab; hs-x status --project <id> for the count and active id, hs-x list for the latest promotion timestamp) | The CLI, at deploy time |
| Is that still what’s running? | Drift — a verdict comparing the latest runtime attestation against the recorded revision | Control plane (Drift tab, hs-x drift) | The runtime, riding live requests at most every 15 minutes |
| What is it doing? | Checkpoints — per-invocation outcome, duration, error fingerprint | Analytics + sampled exemplars (Checkpoint tab) | The runtime, on every capability invocation |
The order matters. The deploy revision is the contract; drift is the verification of that contract; checkpoints are the behavior under it. A project header only reads HEALTHY when the verification has actually run — before the first attestation lands it reads awaiting first attestation, because healthy is a verdict, not a default.
Follow one deploy through its lifecycle
Run a deploy against Email Guard and watch what the control plane records:
hs-x deploy --yesBehind one command, the revision moves through explicit states:
planned— the control plane allocates the deploy id and issues short-lived credential leases for the HubSpot and Cloudflare legs. Nothing user-visible changed yet.recorded— the code is live on Cloudflare and the control plane has the receipt: manifest hash, the captured Cloudflare Worker version id (and its binding fingerprint), and provenance (CLI version, git commit and branch, who deployed). Recording is the default on control-plane-backed deploys;--no-recordopts out, and skipping it means the Worker's attestations are rejected by the control plane (404 deploy_record_not_found), so the project never gets a drift verdict for that deploy — the header stays onawaiting first attestation(or the previous deploy's stale verdict) and the deploy can never be promoted (see step 3).promoted— the revision becomes the active deploy: the routing pointer for its environment now names it. Promotion is gated on a healthy attestation, so the usual shape ishs-x deploy --promote-when-healthy, which records, waits for the runtime’s first healthy heartbeat, then promotes.retired— a later deploy was promoted over it. Retired (and rolled-back) revisions that carry a captured Worker version are your rollback candidates.rolled-back— it was active and you explicitly reverted away from it withhs-x rollback.
Two consequences follow from that state machine. First, recorded ≠ active: you can record ten revisions and promote one. The Deploys tab’s “active deploy” card is the routing pointer, not the newest row. Second, retention is bounded: HS-X keeps the active revision plus the three most recent retired or rolled-back ones, and prunes older terminal revisions (the dashboard’s “retention: 3 prior + current” line). Rollback restores live Cloudflare traffic to one of those retained revisions’ captured Worker version, then re-promotes its record:
hs-x rollback # back to the most recent retired revision
hs-x rollback --deploy-id deploy_email_guard_007 # or name one; non-adjacent targets need --forceRead your deploy history
hs-x list shows every project on your account with its last deploy timestamp, which is populated when a revision is promoted; to see how many revisions are recorded (and which one is active), use hs-x status --project <id>:
hs-x list$ hs-x list hs-x list v0.4.1 · acme-main ──────────────── │ email-guard Email Guard last deploy 2026-06-10T18:21:33.009Z │ pricing-cards Pricing Cards no recorded deploys · Use an id with project-scoped commands: `hs-x logs --project <id>`. · "no recorded deploys" means the control plane has no deploy revisions — deploys made while unlinked are not recorded (the app may still be live). ✓ 2 projects in 412ms
The last-deploy column is derived from control-plane deploy records and nothing else. It is deliberately not a claim about whether the app is live in HubSpot, and a project with recorded-but-never-promoted revisions still reads no recorded deploys here.
That distinction is the single most common source of confusion, so here is the rule: “no recorded deploys” means the control plane never got a receipt, not that nothing was shipped. The unlinked path is fully functional — hs-x deploy without a linked HS-X account ships real workers and installable apps, and records its history in your tenant state store (a KV namespace in your own Cloudflare account), never contacting the platform. The control plane only learns about those deploys when you link:
hs-x linkbackfills history. At claim time the CLI reads your local deploy history and registers each entry that reacheddeployedas a history-only revision (source: link-backfill). Your timeline becomes truthful retroactively.- Backfilled revisions are visibility, not rollback targets. No Cloudflare Worker version was ever captured for them, so the Deploys tab lists them as
no revision · history-onlyandhs-x rollbackrefuses to target them (409 rollback_target_has_no_revision).
Read a drift verdict
A recorded revision is a claim; drift is the recurring check of that claim. On its first request (and at most every 15 minutes after, riding live requests through waitUntil — an idle Worker does not attest), the deployed worker posts a signed attestation: its deploy id, the live Cloudflare version id, the manifest hash it is actually running, a fingerprint of its live bindings, its SDK version, and the count of HS-X-tagged resources it can see. The control plane first checks that the attestation belongs to something it knows about, then diffs it against the recorded revision and stores a verdict.
Two rejections happen before any verdict is written, and both are worth knowing because they leave no trace on the Drift tab:
404 deploy_record_not_found— the attested deploy id has no record. This is what--no-recordproduces: the Worker keeps attesting, the control plane keeps refusing, and the header stays onawaiting first attestation(or on the previous deploy's stale verdict).409 attestation_revision_mismatch— the record exists, but the live Cloudflare version id is not the one the record captured. This is what a manualwrangler deployover an HS-X-recorded Worker produces.
Attestations that pass those checks are classified:
| State | What it means | Usual cause |
|---|---|---|
healthy | Attestation matches the recorded revision | The steady state |
drifted | A deploy that has left recorded (promoted, retired, or rolled back) is attesting but is no longer the active route for its environment | Live traffic moved outside the control plane's pointer |
unknown_code | The attested manifest hash does not match the recorded revision | Effectively unreachable today; unrecorded or re-uploaded Workers are rejected at the endpoint instead and never update the verdict |
resource_missing | The attestation reported zero HS-X-tagged resources | Provisioning failed, or the tagged-resource count variable was dropped from the Worker |
credential_revoked, billing_untrusted, and unknown are declared in the schema but nothing produces them today; a project with no attestation shows awaiting first attestation, not unknown. Bindings are recorded in the attestation but not compared for drift.
Read it from either surface:
hs-x drift --project email-guard # latest verdict + reason (the per-resource view is the dashboard Drift tab)
hs-x status --project email-guard # routing + drift + checkpoint in one viewThree things drift is not (the dashboard repeats these for a reason): it is not a deploy failure — the app is serving traffic; it is not a permission gate — rollback still works; it is not an error — it is a signal that live state diverged from the recorded contract. The project header shows ADVISORY, your users see nothing.
The repair is almost always the same: run hs-x deploy again. The deploy records a fresh revision matching what is now live, the next attestation matches it, and the verdict returns to healthy — then promote it (or deploy with --promote-when-healthy in the first place). If the drift was caused by something added or deleted by hand in Cloudflare, remove or restore it and let the next heartbeat confirm.
Read the checkpoint stream
Deploy revisions and drift describe the app at rest. Checkpoints describe it under load: every capability invocation — each validate-email workflow-action run, each email-health card fetch — emits one checkpoint with its outcome, duration, and (on failure) a stable error fingerprint.
The stream has two tiers, and the dashboard’s Checkpoint tab reads both:
- Metrics, unsampled. Every invocation lands in the analytics store. This is what the success-rate number, the p50/p95/p99 latency percentiles, and the hourly trend charts are computed from. (Billing usage is a separate ledger, written by metered capabilities, not derived from this stream.)
- Exemplars, sampled. A sample of concrete invocations (errors by default) is kept with redacted payloads, grouped by error fingerprint. This is the “show me one failing request” layer under the recent-failures table.
One naming collision to defuse: the project header’s awaiting first attestation line is about drift (the runtime hasn’t heartbeated yet), not about this tab. Checkpoints start flowing with the first real invocation; attestation starts with the worker’s first request (--promote-when-healthy sends that request for you by pinging /_hsx/health). A freshly promoted app whose only traffic was that health ping will attest healthy while its Checkpoint tab is still empty — both are telling the truth.
For the layer below checkpoints — raw structured logs, request tracing, alerts — continue to the monitoring guide.
Resolve the common confusions
My app is live and installable, but hs-x list says “no recorded deploys”
It was deployed while unlinked (or with --no-record), so the control plane never got the receipt. The app is fine. Run hs-x link — the claim backfills your local deploy history into the control plane and the timeline fills in.
The header still says “awaiting first attestation” after I deployed with --no-record
The worker is attesting a deploy id the control plane has no record of, so every attestation is rejected with 404 deploy_record_not_found and no verdict is ever written. Control-plane-backed deploys record by default, so the cause is the explicit --no-record. Re-run hs-x deploy without it (and promote when healthy); the first request after that deploy writes the verdict. The sibling case is a manual wrangler deploy over an HS-X-recorded Worker: the record exists but its captured version id no longer matches, so attestations are rejected with 409 attestation_revision_mismatch and the fix is the same.
The dashboard shows five deploys but only two rollback candidates
Candidates are retained terminal revisions (retired or rolled-back) that still carry a captured Cloudflare Worker version. History-only backfilled revisions are listed for truth but are never targets, and a never-promoted recorded revision is not counted as a candidate either, though hs-x rollback --deploy-id will accept it if it carries a captured version.
The header says “awaiting first attestation”
The runtime hasn’t heartbeated yet. Right after a first deploy this clears on the Worker’s first request (--promote-when-healthy sends one for you); if it persists, the worker either isn’t receiving traffic, was deployed without heartbeat wiring (--no-heartbeat), or was deployed with --no-record (previous confusion). hs-x status --project <id> shows whether any attestation has ever landed (Drift: no snapshot recorded when none has).
