diff --git a/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx b/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx
new file mode 100644
index 0000000000..a8b5b1fbb9
--- /dev/null
+++ b/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx
@@ -0,0 +1,139 @@
+---
+title: Unattended scheduling & failure alerting
+description: Run the miner's scheduled commands -- manage poll and discover -- unattended on cron or systemd, and alert reliably when a run fails.
+---
+
+Operational guidance for running the miner's scheduled commands — `manage poll` and `discover` —
+unattended on a timer (cron or systemd), and for alerting when a run fails. These are the two
+commands most likely to run on a schedule; everything they need is local and they make no
+interactive prompts.
+
+
+ Scope: scheduling + failure alerting for `manage poll` / `discover`. For local-state recovery
+ see the [AMS operations runbook](/docs/ams-operations-runbook); for deployment layout see the
+ [AMS deployment guide](/docs/ams-deployment).
+
+
+## The exit-code contract (what to alert on)
+
+Both commands follow the same convention, so any scheduler can detect a failed run from the exit
+code:
+
+
+
+For scheduled runs, two flags matter:
+
+- `--no-update-check` (or `LOOPOVER_MINER_NO_UPDATE_CHECK=1`) — skip the npm-registry version
+ nudge so an unattended run never depends on / prints it.
+- `--json` — machine-parseable stdout, so an alert handler can attach the structured output.
+
+## cron
+
+
+
+Two cron facts to get right here:
+
+
+ cron emails whatever a job writes to stdout/stderr to `MAILTO` — it does not send a message
+ "because" the exit code was non-zero. A job that fails *silently* (non-zero exit, no output)
+ produces no mail, so don't rely on `MAILTO` alone as the failure signal.
+
+
+
+ `logger` succeeds (exit 0), so `cmd || logger …` makes the whole cron job exit 0 — any
+ exit-status-based monitoring then sees success. Capture the code first (`status=$?`) and
+ re-raise it (`exit "$status"`) as shown, so the real failing code survives.
+
+
+## systemd (service + timer)
+
+A `oneshot` service plus a timer is the more observable option: `systemctl status` /
+`journalctl` capture each run, and `OnFailure=` is a first-class alerting hook.
+
+`loopover-miner-discover.service`:
+
+
+
+`loopover-miner-discover.timer`:
+
+
+
+Enable with `systemctl enable --now loopover-miner-discover.timer`.
+
+## Alerting on failure
+
+Every option keys on the same exit-code contract (`2` = failure).
+
+**cron.** Append `|| { status=$?; ; exit "$status"; }` (as in the cron example
+above) — capture `$?` before the alert command runs and re-raise it, so the failure isn't masked.
+Substitute `logger` with a webhook `curl`, a PagerDuty/Slack CLI, etc. (`MAILTO` still mails any
+output, but is not a reliable signal for a silent failure — see the cron note above.)
+
+**systemd.** `OnFailure=loopover-miner-alert@%n.service` runs a templated alert unit on any
+non-zero exit. A minimal alert unit:
+
+
+
+**Wrapper script.** For any scheduler, wrap the command and preserve its exit code:
+
+
+
+Keep `--json` on scheduled runs so the alert handler can forward the structured output; the
+human-readable form is for interactive use.
diff --git a/apps/loopover-ui/src/components/site/docs-nav.tsx b/apps/loopover-ui/src/components/site/docs-nav.tsx
index facbf7cc43..6985b1b972 100644
--- a/apps/loopover-ui/src/components/site/docs-nav.tsx
+++ b/apps/loopover-ui/src/components/site/docs-nav.tsx
@@ -78,6 +78,7 @@ export const docsNav: DocsGroup[] = [
{ to: "/docs/ams-deployment", label: "Deployment guide" },
{ to: "/docs/ams-operations-runbook", label: "Operations runbook" },
{ to: "/docs/ams-observability", label: "Observing your miner" },
+ { to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" },
],
},
],
diff --git a/apps/loopover-ui/src/routeTree.gen.ts b/apps/loopover-ui/src/routeTree.gen.ts
index c1edde8e3f..ba67b6cf3b 100644
--- a/apps/loopover-ui/src/routeTree.gen.ts
+++ b/apps/loopover-ui/src/routeTree.gen.ts
@@ -57,6 +57,7 @@ import { Route as DocsGithubAppRouteImport } from './routes/docs.github-app'
import { Route as DocsFumadocsSpikeApiReferenceRouteImport } from './routes/docs.fumadocs-spike-api-reference'
import { Route as DocsBranchAnalysisRouteImport } from './routes/docs.branch-analysis'
import { Route as DocsBetaOnboardingRouteImport } from './routes/docs.beta-onboarding'
+import { Route as DocsAmsUnattendedSchedulingRouteImport } from './routes/docs.ams-unattended-scheduling'
import { Route as DocsAmsOperationsRunbookRouteImport } from './routes/docs.ams-operations-runbook'
import { Route as DocsAmsObservabilityRouteImport } from './routes/docs.ams-observability'
import { Route as DocsAmsDeploymentRouteImport } from './routes/docs.ams-deployment'
@@ -331,6 +332,12 @@ const DocsBetaOnboardingRoute = DocsBetaOnboardingRouteImport.update({
path: '/beta-onboarding',
getParentRoute: () => DocsRoute,
} as any)
+const DocsAmsUnattendedSchedulingRoute =
+ DocsAmsUnattendedSchedulingRouteImport.update({
+ id: '/ams-unattended-scheduling',
+ path: '/ams-unattended-scheduling',
+ getParentRoute: () => DocsRoute,
+ } as any)
const DocsAmsOperationsRunbookRoute =
DocsAmsOperationsRunbookRouteImport.update({
id: '/ams-operations-runbook',
@@ -457,6 +464,7 @@ export interface FileRoutesByFullPath {
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
+ '/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
@@ -523,6 +531,7 @@ export interface FileRoutesByTo {
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
+ '/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
@@ -593,6 +602,7 @@ export interface FileRoutesById {
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
+ '/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute
'/docs/beta-onboarding': typeof DocsBetaOnboardingRoute
'/docs/branch-analysis': typeof DocsBranchAnalysisRoute
'/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute
@@ -664,6 +674,7 @@ export interface FileRouteTypes {
| '/docs/ams-deployment'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
+ | '/docs/ams-unattended-scheduling'
| '/docs/beta-onboarding'
| '/docs/branch-analysis'
| '/docs/fumadocs-spike-api-reference'
@@ -730,6 +741,7 @@ export interface FileRouteTypes {
| '/docs/ams-deployment'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
+ | '/docs/ams-unattended-scheduling'
| '/docs/beta-onboarding'
| '/docs/branch-analysis'
| '/docs/fumadocs-spike-api-reference'
@@ -799,6 +811,7 @@ export interface FileRouteTypes {
| '/docs/ams-deployment'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
+ | '/docs/ams-unattended-scheduling'
| '/docs/beta-onboarding'
| '/docs/branch-analysis'
| '/docs/fumadocs-spike-api-reference'
@@ -1192,6 +1205,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsBetaOnboardingRouteImport
parentRoute: typeof DocsRoute
}
+ '/docs/ams-unattended-scheduling': {
+ id: '/docs/ams-unattended-scheduling'
+ path: '/ams-unattended-scheduling'
+ fullPath: '/docs/ams-unattended-scheduling'
+ preLoaderRoute: typeof DocsAmsUnattendedSchedulingRouteImport
+ parentRoute: typeof DocsRoute
+ }
'/docs/ams-operations-runbook': {
id: '/docs/ams-operations-runbook'
path: '/ams-operations-runbook'
@@ -1381,6 +1401,7 @@ interface DocsRouteChildren {
DocsAmsDeploymentRoute: typeof DocsAmsDeploymentRoute
DocsAmsObservabilityRoute: typeof DocsAmsObservabilityRoute
DocsAmsOperationsRunbookRoute: typeof DocsAmsOperationsRunbookRoute
+ DocsAmsUnattendedSchedulingRoute: typeof DocsAmsUnattendedSchedulingRoute
DocsBetaOnboardingRoute: typeof DocsBetaOnboardingRoute
DocsBranchAnalysisRoute: typeof DocsBranchAnalysisRoute
DocsFumadocsSpikeApiReferenceRoute: typeof DocsFumadocsSpikeApiReferenceRoute
@@ -1424,6 +1445,7 @@ const DocsRouteChildren: DocsRouteChildren = {
DocsAmsDeploymentRoute: DocsAmsDeploymentRoute,
DocsAmsObservabilityRoute: DocsAmsObservabilityRoute,
DocsAmsOperationsRunbookRoute: DocsAmsOperationsRunbookRoute,
+ DocsAmsUnattendedSchedulingRoute: DocsAmsUnattendedSchedulingRoute,
DocsBetaOnboardingRoute: DocsBetaOnboardingRoute,
DocsBranchAnalysisRoute: DocsBranchAnalysisRoute,
DocsFumadocsSpikeApiReferenceRoute: DocsFumadocsSpikeApiReferenceRoute,
diff --git a/apps/loopover-ui/src/routes/docs.ams-unattended-scheduling.tsx b/apps/loopover-ui/src/routes/docs.ams-unattended-scheduling.tsx
new file mode 100644
index 0000000000..681e913dbe
--- /dev/null
+++ b/apps/loopover-ui/src/routes/docs.ams-unattended-scheduling.tsx
@@ -0,0 +1,49 @@
+import { createFileRoute, notFound } from "@tanstack/react-router";
+import { Suspense } from "react";
+
+import { DocsPage } from "@/components/site/docs-page";
+import { docsClientLoader } from "@/lib/docs-client-loader";
+
+// Rendered from content/docs/ams-unattended-scheduling.mdx via fumadocs-mdx's browser entry
+// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
+// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
+// for why the loader below resolves only a plain, serializable path string.
+export const Route = createFileRoute("/docs/ams-unattended-scheduling")({
+ loader: async () => {
+ const { docsSource } = await import("@/lib/docs-source");
+ const page = docsSource.getPage(["ams-unattended-scheduling"]);
+ if (!page) throw notFound();
+ return { path: page.path, title: page.data.title, description: page.data.description };
+ },
+ head: () => ({
+ meta: [
+ { title: "Unattended scheduling & failure alerting — LoopOver docs" },
+ {
+ name: "description",
+ content:
+ "Run the miner's scheduled commands -- manage poll and discover -- unattended on cron or systemd, and alert reliably when a run fails.",
+ },
+ { property: "og:title", content: "Unattended scheduling & failure alerting — LoopOver docs" },
+ {
+ property: "og:description",
+ content:
+ "Run the miner's scheduled commands -- manage poll and discover -- unattended on cron or systemd, and alert reliably when a run fails.",
+ },
+ { property: "og:url", content: "/docs/ams-unattended-scheduling" },
+ ],
+ links: [{ rel: "canonical", href: "/docs/ams-unattended-scheduling" }],
+ }),
+ component: AmsUnattendedScheduling,
+});
+
+function AmsUnattendedScheduling() {
+ const { path, title, description } = Route.useLoaderData();
+ const Content = docsClientLoader.getComponent(path);
+ return (
+
+ Loading…
}>
+
+
+
+ );
+}
diff --git a/apps/loopover-ui/src/routes/docs.index.tsx b/apps/loopover-ui/src/routes/docs.index.tsx
index 5834681179..3d402be49e 100644
--- a/apps/loopover-ui/src/routes/docs.index.tsx
+++ b/apps/loopover-ui/src/routes/docs.index.tsx
@@ -76,6 +76,7 @@ const AUDIENCES: Audience[] = [
{ to: "/docs/ams-deployment", label: "AMS deployment guide" },
{ to: "/docs/ams-operations-runbook", label: "AMS operations runbook" },
{ to: "/docs/ams-observability", label: "Observing your miner" },
+ { to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" },
{ to: "/docs/self-hosting-docs-audit", label: "Self-host docs audit" },
{ to: "/docs/maintainer-install-trust", label: "Install & trust guide" },
{ to: "/docs/github-app", label: "GitHub App configuration" },
diff --git a/packages/loopover-miner/docs/unattended-scheduling.md b/packages/loopover-miner/docs/unattended-scheduling.md
index cf9f9ca513..589551bb52 100644
--- a/packages/loopover-miner/docs/unattended-scheduling.md
+++ b/packages/loopover-miner/docs/unattended-scheduling.md
@@ -1,5 +1,10 @@
# loopover-miner — unattended scheduling & failure alerting
+> Also published on the docs website: [Unattended scheduling & failure
+> alerting](https://loopover.ai/docs/ams-unattended-scheduling) (same content, rendered with
+> search and the rest of the maintainer docs nav). This file remains the canonical source and
+> ships inside the published `@loopover/miner` package.
+
Operational guidance for running the miner's scheduled commands — `manage poll` and `discover` —
unattended on a timer (cron or systemd), and for alerting when a run fails. These are the two commands
most likely to run on a schedule; everything they need is local and they make no interactive prompts.