Elite Outsiders ← Tools menu Updated 22-09-2026

Robots

Every automation that runs on its own. A "cron" is just the technical word for a scheduled wake-up: a robot = a program + its wake-up. Families below: Cloudflare robots (CRON1-20), n8n robots on the VPS (publication), local Mac robots, routines and event triggers.

22-09-2026—UPDATE
eo-backup-worker CF · maintained by Claude Code
▶ ELITE OUTSIDERS WORKERSread live from the Cloudflare API
The Cloudflare Workers of Elite Outsiders, plus the ones shared across the 5 zones · list and attached crons read from the API at each load, never declared · repo and role come from a map in functions/api/admin/workers.js · the other brands list theirs on their own /crons
WorkerBrandCrons attachedLast deployRepoRole
Loading the account…

▶ CRONS18 cards · 16 CF Workers + 2 local launchd
CF Workers — 8 deployed

CRON18 — Unused pages

CRON MONTHLY 01st @ 06h UTC
06:00 UTC on the 1st (= 13h Vietnam) · eo-backup-worker/src/unused-pages.js · added 2026-09-09 (rule 12, Intranet structure)

Lists the intranet pages with no visit in 90 days (tools_pages minus tools_visits) and the public paths gone quiet (events), one email to AL. Read-only on D1.

Technical details
  • Source data: tools_visits, fed by the beacon in /assets/eo-theme.js (one hit per page, per browser, per day, never from an iframe) and tools_pages, rewritten at every deploy by scripts/pages-inventory.js.
  • The same numbers sit next to every link on /alllinks (grey: last visit and 90-day count, red: no visit in 90 days) and behind GET /api/visits?days=90.
  • Manual trigger: POST /admin/unused-pages-now?token=… on the worker, &dry=1 computes without sending.

CRON1 — Security backup

CRON MONTHLY 01st @ 03h UTC
03:00 UTC (= 10h Vietnam)

Monthly Kit + D1 backup to private R2. Quarterly dry-run on preview DB. Email recap.

Technical details
  • Photocopies the Kit newsletter (all subscribers + tags) and the site DB (D1), stores 4 files in a private Cloudflare R2 drawer. Like a family photo kept safe.
  • 12-month retention. On Jan/Apr/Jul/Oct 1st, runs a restore dry-run on a preview DB to verify backups are readable (quarterly dry-run).
  • Each run sends a Resend recap email (✅ ok / ⚠️ partial / ❌ failed) listing what was backed up.
cron: 0 3 1 * * · R2 target: eo-backups bucket · doc: Notion ch. 13 (Monitoring/alerting) + sub-page Setup backup mensuel Kit + D1 — 2026-04-30

CRON2 — Cal.com slot watcher

CRON DAILY 07h UTC
07:00 UTC (= 14h Vietnam)

Daily check that Cal.com slots are bookable across the 3 brands. Email if any slot is dead.

Technical details
  • Watches the 3 public Cal.com pages (byCaliber / Forja / EO) and counts available slots over the next 4 weeks.
  • If a slot is dead (another booking blocks it), sends a "DEAD" email so you can decide what to do.
  • If you fix the conflict (or weekly chaos moves the slot), you get a "REVIVED" email at next check. No email when all is well — radio silence.
  • Memory in Cloudflare KV (key cal_slot_health:v2) — stores state per (event × date) to detect transitions.
cron: 0 7 * * * · code: eo-backup-worker/src/cal-monitor.js · manual trigger: POST /admin/cal-monitor-now?token=...

CRON3 — Weekly chaos magician

CRON STANDBY stopped 23/08/2026
STANDBY since 2026-08-23 · AL decision · was SUN 21:00 UTC

Stopped. Used to re-randomize Cal.com slot times and rotate the 3 brands across 4 weeks ahead. Now returns immediately without touching any schedule.

Technical details
  • Why it was stopped. Forja moved to a two-event international setup on 23/08/2026 (EU + Americas, and APAC) with dedicated schedules 2273434 and 2273427 that this robot does not know about. Leaving it running would have kept rewriting three schedules nobody uses for Forja any more.
  • Second reason. The STARTS hours were tuned against AL's Google Calendar, now read in Vietnam time (email routines 09:00-09:45, Posting 18:00-18:30, evening emails 20:30-21:00). AL has lived in Vietnam since June 2026, so those bounds are a leftover, not a real constraint.
  • How the stop is implemented. A STANDBY = true constant at the top of src/cal-randomize.js. The function logs and returns before any Cal.com call. The cron trigger itself is untouched in wrangler.toml, so re-arming is a one-line flip plus a redeploy.
  • Careful with the Run now button above. It still fires the endpoint, which now returns {"standby": true} and does nothing. That is expected, not a failure.
  • Historic behaviour, for reference: randomized MON afternoon / WED morning / FRI evening over 4 weeks and rotated byCaliber / Forja / EO across the 3 daily slots, to make the calendar look like a real busy life rather than three fixed slots.
cron: 0 21 * * SUN · code: eo-backup-worker/src/cal-randomize.js · manual trigger: POST /admin/cal-randomize-now?token=...

CRON4 — Weekly report

CRON WEEKLY MON 08h UTC
08:00 UTC (= 15h Vietnam) · LIVE since 2026-05-01

Monday morning report: 7-day counters + delta vs prev 7d + top pages, posted to Notion + email.

Technical details
  • Every Monday morning, reads the DB directly (D1 binding, not via dashboard — gated by CF Access), builds a Markdown with 7d counters + delta vs prior 7d + top 10 pages, and creates a Notion sub-page under the operational hub. Then sends a recap email to alexandre.corne.ac@gmail.com (override via NOTIFY_TO_WEEKLY).
  • Resilience: if Notion is down (token/parent_id missing or integration not connected), the email goes out anyway with the markdown embedded — AL never loses the info.
  • Code: eo-backup-worker/src/weekly-snapshot.js + Notion helper src/notion.js. Email subject: EO weekly — YYYY-WXX.
cron: 0 8 * * 1 · code: eo-backup-worker/src/weekly-snapshot.js · manual trigger: POST /admin/weekly-snapshot-now?token=...

CRON5 — Monthly mover

CRON MONTHLY 01st @ 02h UTC
02:00 UTC · LIVE since 2026-05-01

Monthly archive of prior-month events to R2 + Notion, then DELETE old rows from D1.

Technical details

⚠ Heads-up — this cron DELETEs rows from the live events D1 table after archiving them to R2. Safe in normal conditions (R2 backup-first, current-month events untouched, R2-fail-HARD prevents data loss), but it's the only cron on this page that touches data destructively. Yellow button = think before you click.

  • On the 1st of each month at 02h UTC (1h BEFORE the monthly backup 0 3 1 * * — by design so the R2 dump captures an already-cleaned table), takes prior-month events, dumps JSON to R2 (eo-backups/events-archive/YYYY-MM/), creates a Notion archive page with aggregations (event_type / top paths / activity per day), then only after that runs DELETE FROM events WHERE created_at < <current month start - 6 months> (6-month rolling retention in D1 since 2026-08-27; the R2 dump still covers everything before the current month, a strict superset of the delete).
  • Why: the D1 base grows fast (5,518 rows in 15 pre-launch days). Without cleanup we pay to store data we never look at live — aggregates land in the Notion page + the weekly report.
  • Manual precedent: Analytics Archive — Pre-2026-05-01 (5,518 rows archived 2026-04-30) — format mirrored by renderNotionBlocks().
  • Absolute safety (backup-first): R2 fail = HARD, the DELETE doesn't run. Notion fail = soft, the DELETE runs anyway (the JSON on R2 is the official safety net; Notion is read comfort). The recap email always shows the exact state of the 3 steps (R2 / Notion / DELETE).
  • Pull an archive: wrangler r2 object get eo-backups/events-archive/2026-04/events-archive-2026-05-01.json
cron: 0 2 1 * * · code: eo-backup-worker/src/events-archive.js · manual trigger: POST /admin/events-archive-now?token=... (idempotent on mid-month clicks; the last 6 months plus the current month are never touched, R2-fail-HARD prevents data loss if backup fails)

CRON6 — Page Flow crawler

CRON WEEKLY MON 04h UTC
04:00 UTC (= 11h Vietnam) · LIVE since 2026-05-03

Monday crawl of eliteoutsiders.com sitemap, upsert page_inventory + button_inventory in D1.

Technical details
  • Every Monday at 04h UTC, this robot fetches eliteoutsiders.com/sitemap.xml, walks each public page, and extracts every clickable element (<a> / <button> / [role="button"] / [data-track-id]). Result: complete inventory of pages + buttons in D1 (page_inventory + button_inventory), even buttons that have never been clicked.
  • Runs 4h BEFORE the weekly-snapshot (08h UTC) so the weekly report benefits from a freshly updated inventory.
  • Automatic stale-cleanup: any page removed from the sitemap is removed from the inventory (with its buttons). Skipped if the sitemap fails (guard against accidental wipe).
  • Read by tools.eliteoutsiders.com/page-flow (User Journey / Traffic Flow). Also triggerable manually via the "Refresh now" button on that page.
cron: 0 4 * * 1 · code: eo-backup-worker/src/page-flow-crawl.js · manual trigger: POST /admin/page-flow-crawl-now?token=...

CRON7 — URL audit

CRON MONTHLY 01st @ 04h UTC
04:00 UTC · LIVE since 2026-05-03 (split off from the monthly backup that same day)

Monthly crawl of every known URL (open + me + admin + tools), email anomaly report.

Technical details
  • On the 1st of each month at 04h UTC (1h after the backup 0 3 1 * * to avoid loading the network during the R2 upload), this robot crawls every known URL of the site (open + me + admin + tools), compares against the EXPECTED baseline in the code, and sends a Resend email summary to AL.
  • Performs ZERO auto-fix. Pure audit: "here are the URLs that respond differently than expected, up to you whether you fix or update the baseline".
  • To extend the URL list: edit EXPECTED in eo-backup-worker/src/url-audit.js.
cron: 0 4 1 * * · code: eo-backup-worker/src/url-audit.js · manual trigger: POST /admin/url-audit-now?token=...

CRON8 — Analytics map refresh

CRON SEMI-ANNUAL 01/01 + 01/07 05h
05:00 UTC · LIVE since 2026-05-05

Semi-annual snapshot of D1 row counts per analytics table, append to Notion source-of-truth.

Technical details
  • Twice a year (1st of January + 1st of July, 05:00 UTC), this robot queries D1 for row counts + last-write timestamp on every analytics table, and the breakdown of event_type values in the events table (30d + total). It then appends a snapshot block to the Notion source-of-truth page 📊 EO Analytics — Source of truth so AL has a history of how the analytics stack has grown over time.
  • Companion live mirror: tools.eliteoutsiders.com/backdata/analytics-map queries D1 in real time on every page load (no scheduled refresh needed for the live numbers — they're always fresh).
  • To extend the inventory: edit the TABLES list in eo-backup-worker/src/analytics-map-refresh.js AND the mirror in eliteoutsiders-tools/functions/api/admin/analytics-map.js. Keep both in sync.
cron: 0 5 1 1,7 * · code: eo-backup-worker/src/analytics-map-refresh.js · manual trigger: POST /admin/analytics-map-refresh-now?token=...
Local launchd Mac — 2

CRON9 — WIP safety net

CRON EVERY 30 MIN LOCAL LAUNCHD
Local launchd · LIVE since 2026-05-02

Every 30 min, rsync uncommitted changes from 3 working dirs to wip-autosave GitHub branch.

Pulse: loading…
Technical details
  • Every 30 min, this robot looks in your 3 working dirs (eliteoutsiders-site, eliteoutsiders-tools, eo-backup-worker) and copies anything not yet in Git into a parallel GitHub box called wip-autosave. Like a photocopier passing through your office every 30 min, photographing what's lying on the table without touching anything. If your Mac dies or you close by mistake, max 30 min of lost work.
  • Architecture: 3 mirror clones live in ~/Library/Application Support/eo-wip-autosave/. Each run rsyncs your working tree (excluding node_modules, .wrangler, dist, logs) to its mirror, commits + pushes to the wip-autosave branch (never main). Your main working tree is NEVER touched. No main history pollution.
  • Recover a copy: git fetch origin wip-autosave && git checkout wip-autosave in any clone of the repo, or directly on GitHub. Commits are timestamped: WIP autosave 2026-05-02T01:39:59Z.
cron: 0/30 * * * * (StartInterval 1800s launchd) · script: eo-backup-worker/scripts/wip-autosave.sh · plist: ~/Library/LaunchAgents/com.eo.wip-autosave.plist · log: ~/Projects/eo-backup-worker/logs/wip-autosave.log

CRON13 — V folder namer CONTENT

CRON HOURLY LOCAL LAUNCHD
Local launchd · LIVE since 2026-07-26

Every hour, reads the Long Form Content Planner in Notion and makes the Finder video folders match it: every planner row with Status Ongoing or Planned gets a folder named V{N} - {title} inside videos/ongoing/. New row → folder created from _template. Title changed in Notion → folder renamed. Never deletes anything.

Technical details
  • Why: AL's rule is "1 planner row = 1 folder, name carries the video number + current title". Before this cron the folder names drifted from the planner titles silently (V8 was created by hand). Now Notion is the single source of the name and Finder follows within the hour.
  • Source of truth: Notion Long Form Content Planner DB 04428224657182ee8f3d81fc0fbe50b0, rows with Status = Ongoing or Planned. Folder name = V{N} - sanitize(title) (slashes/colons stripped, length capped).
  • Behavior: missing folder → created by copying _template · folder found under an old title → renamed in place (contents untouched) · row removed or status changed → folder LEFT ALONE (no deletion, ever — cleanup stays a human decision).
  • Closed loop since 2026-08-09: when a run reaches 100% on the video board, PATCH /api/admin/video-runs/:id flips the matching planner row to Status = Posted (functions/_lib/notion-planner.js), so the row drops out of the Ongoing/Planned filter this cron reads and no empty V{N} shell is recreated for an already published video (the V8 - Beating Women shell used to come back every hour). Match is exact on the V number parsed from the run label, never on the title; 0 or 2+ candidates write nothing and log the reason; the Notion call is fire-and-forget and never fails a board save; only the transition to completed fires it, past runs are not backfilled.
  • Known limit (audit 26/07, finding 10): FINDER buttons on the LF board store absolute paths; a rename here does not rewrite them. Mitigation today: board buttons point at stable parents (ongoing/), not at V{N} folders. Fix tracked in backlog.
  • Not on /crons live status: launchd has no pulse endpoint yet (audit finding 22, backlog) — check the log file to confirm the last run.
cron: hourly (StartInterval 3600s launchd) · script: eliteoutsiders-tools/scripts/v-folder-sync.mjs · plist: ~/Library/LaunchAgents/com.eo.v-folder-sync.plist (source eliteoutsiders-tools/scripts/launchd/) · log: ~/Library/Logs/eo-v-folder-sync.log · secret: NOTION_API_KEY from ~/.config/eo/secrets.env
CF Workers — 7 deployed

CRON10 — Google Calendar scanner

CRON EVERY 30 MIN GCAL POLL
Every 30 min · LIVE since 2026-05-05

Every 30 min, scan Google Calendar for mentorship RDVs outside Cal.com, upsert bookings in D1.

Technical details
  • The robot reads your Google Calendar, detects "client mentorship" RDVs (tagged or with participant email matching an EO user), and inserts/updates the row in bookings with is_mentorship=1. So the "Your Bookings" cell turns green in the client's /me without you touching the DB by hand.
  • Why: today bookings rows are created only by the Cal.com webhook. If you book with a client outside Cal.com (manual email, direct GCal event), the client sees nothing in their /me. This robot fills that gap.
  • Prereq: Google Calendar OAuth (table oauth_tokens, row service='google_calendar', migration 0010). The robot refreshes the access token itself when it has expired.
  • Pipeline, 4 steps: read the oauth_tokens row and refresh the access token if needed → list the primary calendar from now to now + 28 days → for each event, match any attendee email against the contacts table (keywords mentorship / mentoring / coaching / EO / elite outsiders also classify) → UPSERT into bookings, idempotent on external_id prefixed gcal:. The is_mentorship marker goes in raw_json because the bookings schema has no dedicated column for it.
  • Secrets required: GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET on the worker. If either is missing the run is skipped with a non-fatal warning in the logs, so the cron never enters an error loop. A silent skip looks exactly like a clean run in the dashboard: check the logs, not the absence of an alert.
cron: */30 * * * * · status: LIVE · source: eo-backup-worker/src/gcal-scan.js · manual: POST /admin/gcal-scan-now?token=$BACKUP_HEALTH_TOKEN (the Run now button above proxies to it) · trigger attached to eo-backup-worker since 2026-05-05, confirmed against the Cloudflare schedules API on 2026-08-27 · card corrected 2026-08-27: it had been reading SPEC / planned cron / code to write for three months while the robot was running

CRON11 — Error events watchdog

CRON DAILY 06:00 UTC D1 → MAIL IF ANY
Daily 06:00 UTC · LIVE since 2026-05-07

Every day, query D1 error_events (last 24h) plus KPI thresholds (bounce rate 30d, quiz completion, visits drop). Transitions-only since 2026-08-27: a breach identical to the previous run stays silent (state in KV), mail fires only when the breach set changes, new error rows arrive, or everything recovers.

Technical details
  • Why: server-side errors logged via functions/_lib/log-error.js (e.g. 500 on /api/admin/trauma-maps/[id]) are silent until AL clicks the broken thing. This watchdog flags them within 24h of occurrence.
  • Sources currently wired: /api/events, /api/auth/magic-link (Resend fail), /api/trauma-map/save (DB error), /api/admin/trauma-maps/[id]. Any new endpoint can opt-in by importing logError and calling it in the catch block.
  • Email shows: total count + breakdown by level + breakdown by source + latest 20 errors with timestamp/level/source/status/path.
  • "Silent on zero" by design — if you go a week without an email, all good. If you want to test, hit the manual trigger.
  • Transitions-only dedup (2026-08-27): last breach fingerprint persisted in KV BACKUP_STATE key error-events-watchdog:v1. Identical breach = suppressed. New error_events rows (max id change), a changed breach set, or a recovery each trigger exactly one mail. Fix for the identical daily bounce_rate alert sent since 2026-05-13.
cron: 0 6 * * * · code: eo-backup-worker/src/error-events-watchdog.js · manual trigger: POST /admin/error-events-watchdog-now?token=$BACKUP_HEALTH_TOKEN

CRON12 — Content sync check

CRONCONTENT WEEKLY MON 09:00 UTC D1 + NOTION → MAIL IF DRIFT
Weekly Monday 09:00 UTC · LIVE since 2026-07-26

Keeps the content databases honest against each other. Reads both boards of the LF cross-platform process from D1, then checks every configured Notion link, every empty PROMPT/URL button, every remaining XXX placeholder and every task with no prompt and no URL. Report stored in D1; email only when something gets worse — never a "all good" email.

Technical details
  • Why: a Notion page that moves or gets deleted turns into a silent dead button on the board. Three such dead IDs were found by hand on 2026-07-25 after a hub reset. This makes it automatic.
  • Checks: dead Notion links · empty enabled buttons · XXX placeholders · tasks with neither prompt nor URL · wave distribution · phases with zero step · SBY parking size. SBY content is excluded from drift on purpose.
  • False-positive guard: the Notion API answers 404 both for a deleted page and for a page not shared with the worker integration. If more than 40% of checked links answer 404, the run flags notion_access: limited, moves them to unverified_links and stays silent instead of crying wolf.
  • To enable real dead-link detection: share the EO Notion hub with the worker's integration (NOTION_API_KEY). Until then the structural checks still run.
  • Persistence alert (added 26/07): if notion_access: limited persists over 2 consecutive runs, the cron emails AL with the fix instruction instead of staying silent — the main check is never allowed to be a silent no-op for weeks.
  • Report readable in D1: SELECT value_json FROM intranet_state WHERE key='content_sync_check'.
cron: 0 9 * * 1 · code: eo-backup-worker/src/content-sync-check.js · manual trigger: POST /admin/content-sync-check-now?token=$BACKUP_HEALTH_TOKEN

CRON14 — Notion freshness scanner

CRON DAILY 10:00 UTC NOTION → D1
Daily 10:00 UTC · LIVE since 2026-07-28

Kills the "frozen page" problem on /architecture section A. Every day it asks the Notion API for the REAL last-edited date of every Notion page linked in that section, and stores it in D1. The page then shows a dated dot next to each link: grey = untouched, gold = edited after AL last ticked "Up to date" (re-read it), red = the hand-typed date in the Notion title is older than the real edit (the stamp lies).

Technical details
  • Why: AL hand-dates every Notion page title so staleness is visible at a glance, but nothing propagated that to the architecture page. A prompt updated in a week would have shown as current here, in silence. This makes the page reflect reality instead of a snapshot.
  • Self-feeding list: on load, /architecture writes the Notion page ids it displays into D1 key architecture_notion_ids. Add a link tomorrow, it is tracked the day after. No manual list to maintain.
  • Output: D1 key notion_freshness (per page: real last_edited_time, current title, and the date parsed out of that title). The page only renders it, it never calls Notion itself.
  • First run (28/07): 82 pages read in 3 seconds, 34 titles whose hand-typed date lags behind the real edit.
  • Known gap: 2 real pages are unreachable because they are not shared with the worker's Notion integration (X posts DB, Threads posts DB). The other 12 misses are Notion column wrappers, not pages, and are expected.
cron: 0 10 * * * · code: eo-backup-worker/src/notion-freshness.js · reads: architecture_notion_ids · writes: notion_freshness · secret: NOTION_API_KEY

CRON15 — Kit sync check EMAIL

CRON MON + THU 05:00 UTC D1 ↔ KIT
Twice a week, Monday and Thursday 05:00 UTC · LIVE since 2026-07-28

Answers one question: is every person who consented actually in Kit? It reads the D1 contacts rows that carry a consent and no revocation, reads the live Kit subscriber list, and compares. Anyone missing is pushed into Kit on the spot and their Kit id is written back to D1. AL only gets an email when something was repaired or could not be repaired. Silence means the two bases agree.

Technical details
  • Why it exists: on 2026-05-28 a real person completed the Trauma Map quiz, ticked the consent box, and never reached Kit. Nobody knew for two months. Root cause: /api/trauma-map/save threw away the result of its Kit call, so a failure left no trace anywhere. Fixed the same day (3 retries + a row in error_events + kit_subscriber_id written back), but a code fix only protects the future. This robot is the net that catches whatever still slips.
  • Test addresses are excluded by pattern (@eliteoutsiders.test, @example.com, smoke-, e2e-, mail-tester, and friends) so the report only ever counts real humans.
  • Three gaps measured: missing in Kit (repaired), present in Kit but no kit_subscriber_id in D1 (back-filled), present in Kit but absent from D1 (reported only, never inserted blind).
  • D1 writes are deliberately narrow: the report key, plus kit_subscriber_id and kit_synced_at on existing rows. No INSERT into contacts, no other column touched.
  • First run (28/07, manual): 14 consenting contacts against 27 Kit subscribers, 3 pushed into Kit including the person lost on 28/05, 1 Kit id back-filled, 0 failures.
cron: 0 5 * * 1,4 · code: eo-backup-worker/src/kit-sync-check.js · reads: contacts + Kit API · writes: kit_sync_check, contacts.kit_subscriber_id · secrets: KIT_API_KEY, RESEND_API_KEY

CRON16 — Newsletter dispatch EMAIL

CRON DAILY 06:30 UTC NOTION → KIT
Daily 06:30 UTC · LIVE since 2026-07-28

The missing link between the newsletter drafts and an actual send. Every morning it reads the Notion base "Weekly Newsletter Per Long-Form Stack", takes every row where AL set Validation = Yes, and creates a SCHEDULED broadcast in Kit at the exact date and time written on the row. AL never opens Kit: he reads the draft in Notion, flips Validation to Yes, and the email leaves on its own at the planned slot. A confirmation email lists what was scheduled.

Technical details
  • Why: before 28/07 nothing read the drafts base. Kit had 3 broadcasts ever (2 April tests + 1 unsent draft). The pipeline wrote newsletters that could not physically leave.
  • Guards, in order of what AL fears most: Validation = Yes is mandatory. The send date must be at least 1 hour in the future, a past date is reported and NEVER sent (no wrong-moment post, ever). A row that already has a Kit Broadcast ID is skipped, so nothing is ever scheduled twice.
  • The body is taken verbatim from the row's Description. The robot converts line breaks and links to email HTML, it never rewrites a word.
  • Columns added to the base on 28/07: Subject, Preview Text, Kit Broadcast ID. Subject and preview live on the row, the row title stays "Newsletter W31" style.
  • First run (28/07, manual): Newsletter W31 scheduled, Kit broadcast 25188287, send Saturday 01/08 14:00 Vietnam (day fixed by AL 28/07: Saturday, Sunday belongs to the long form), all subscribers, content checks clean (0 em-dash, video link present).
cron: 30 6 * * * · code: eo-backup-worker/src/newsletter-dispatch.js · reads: Notion base 5a249cf… · writes: Kit broadcasts, newsletter_dispatch, Notion Kit Broadcast ID · secrets: NOTION_API_KEY, KIT_API_KEY, RESEND_API_KEY

CRON17 — Quotes trigger CONTENT

CRON MON 04:30 UTC CF → N8N
Weekly, Monday 04:30 UTC (11:30 Da Nang) · LIVE since 2026-07-29 · first tick Monday 03/08

The button nobody was pressing. The n8n quotes robot (repaired 28/07) schedules every armed quote, but only a webhook or a manual click could start it. This wake-up presses the button once a week with the shared webhook key. AL's weekly gesture drops to zero: arm rows in Notion, they get scheduled the following Monday. Careful with Run now: it REALLY starts the quotes robot.

Technical details
  • The content guards live in the n8n robot itself: future dates only, past date raises, APPROVED direct, Done + Posted At written back. This cron only presses the button, and mails AL ONLY if the press itself fails (the n8n error notifier covers the rest).
  • Idempotent: rows already Done are never picked up again.
cron: 30 4 * * 1 · code: eo-backup-worker/src/quotes-trigger.js · calls: /webhook/quotes-trigger (header key) · writes: quotes_trigger · secret: N8N_WEBHOOK_KEY

CRON19 — Social snapshot CONTENT

CRON SUN 02:00 UTC CF → YouTube
Weekly, Sunday 02:00 UTC (09:00 Vietnam) · LIVE since 2026-08-26

The dashboard that was never fed. The Social Media section of /coredata shipped in May 2026 with its tables, its endpoint and its Run now button, but no cron ever called it, so the curves only moved on days someone clicked by hand. This wake-up collects the YouTube channel once a week: subscriber count, total views, and the last 50 uploads with their views, likes, comments and duration. Everything else on that dashboard (LinkedIn, Substack, X, Instagram, TikTok, Threads) is still manual entry.

Technical details
  • Same SQL and same metric names as the tools endpoint /api/admin/social/youtube-fetch, so the cron and the Run now button produce indistinguishable rows. If one changes, change the other in the same commit.
  • Writes through the worker EO_DB binding, which is SELECT-only by convention everywhere else. This is the single documented exception, and it touches only social_snapshots and social_posts.
  • Quota: roughly 4 units per run against the YouTube Data API daily budget of 10 000. The expensive search call is a fallback only, used if the channel has no uploads playlist.
  • Shorts versus long is a duration heuristic at 180s, accurate to about 85-90 percent. A vertical video under 3 minutes can be misfiled.
cron: 0 2 * * SUN · code: eo-backup-worker/src/social-snapshot.js · writes: social_snapshots, social_posts · secret: YT_DATA_API_KEY

CRON20 — Planner sync CONTENT

CRON HOURLY :15 Notion → D1
Every hour at :15 UTC · eo-backup-worker/src/content-posts-sync.js · LIVE since 2026-09-17 (AL: tools and Notion planners connected, tracker included)

Pulls the eleven Notion planner bases (long form, shorts, X, LinkedIn, Threads, Substack, Pinterest, newsletter, carousels, quotes, lifestyle) into the D1 table content_posts, so the publication tracker and the This week grid of every planner page show what Notion holds, dates and codes corrected in Notion included. Replaces the manual import, which stays as a fallback.

Technical details
  • Upsert on (code, platform), never a DELETE; a field Notion leaves empty never erases a stored value. Rows without a Code are counted per base, not written. Other D1 rows of the same code and planner row get the new date and status.
  • Status: backup (Status Backup, SBY, Backlog), posted (Done or Posted, or in PostFast with a past date), scheduled (PostFast Post Id, Posted Platforms, Posted At or Kit Broadcast ID with a future date), planned otherwise. Dates rewritten in Vietnam local time.
  • Second documented write exception of the worker EO_DB binding (after CRON19): only content_posts and content_posts_sync_runs (migration 0057, one row per run, read back by GET /api/content-posts as sync).
  • Manual trigger: POST /admin/content-posts-sync-now?token=… on the worker, &dry=1 reads Notion and builds the rows without writing. Fallback: node scripts/content-posts-import.mjs in eliteoutsiders-tools.
cron: 15 * * * * · code: eo-backup-worker/src/content-posts-sync.js · writes: content_posts, content_posts_sync_runs · secret: NOTION_PLANNERS_KEY
▶ N8N ROBOTS (VPS)6 cards · publication robots on the Hostinger VPS, NOT Cloudflare

N8N-1 — Shorts Auto-Publisher ⭐⭐⭐

N8N WEBHOOK + MANUAL NOTION → POSTFAST
Rewritten 28/07/2026 (v2) · zero production runs yet

Reads the Shorts Posting Planning base (rows AL set to Ready), reads posting slots LIVE from content-system, refuses past dates with a visible error, targets YouTube + Instagram + Facebook. TikTok shorts never go through it: they are 100% manual via AL's Thursday routine.

Technical details
  • Webhook closed on 28/07 evening: an anonymous call now gets 403 (header auth). Error notifier wired the same evening.
  • 3 stars on paper, one reserve: it has never run in production. First supervised run planned on 1 test short once the next batch exists.
id: NgJNAYfiTgxqehu2 · instance: 72.62.187.71:5678

N8N-2 — Quotes Weekly ⭐⭐

N8N WEBHOOK + MANUAL NOTION → POSTFAST
Repaired 28/07/2026 (v5) · trigger still manual

Takes every eligible quote (Validation = Yes, Status = In progress, FUTURE Posting Date), schedules the 5 platforms in PostFast at the Notion date, direct APPROVED so AL never approves in PostFast anymore, then writes Done + Posted At. A past-dated eligible row raises a visible error and nothing is published: the silent skip that killed Q10 to Q16 is gone. The banned hashtag was removed from the code.

Technical details
  • Fixed 28/07: 10-day window removed, all eligible rows per run instead of one, Posted At written, APPROVED direct, webhook closed (403), error notifier wired.
  • Why 2 stars: nothing triggers it automatically yet (weekly Cloudflare trigger = next build), and the visuals stock stops at Q22 (07/11/2026).
id: yuWyWuPpjiI77ppd · quote base: c00a94af…e340

N8N-3 — Carousels Auto-Publisher ⭐⭐

N8N WEBHOOK + MANUAL NOTION → POSTFAST
Repaired 28/07/2026 (v3) · planner has 1 ready-to-arm row

Now reads the RIGHT base (Carousels Content Planner: it silently queried the Substack base for months) and publishes at the Notion Posting Date instead of inventing "next Wednesday". Music dropped on carousels (AL 28/07): IG + TikTok + Facebook go out SCHEDULED + APPROVED like LinkedIn / X / Threads, no draft inbox step anymore.

Technical details
  • Fixed 28/07: base repointed to the canonical planner, nextWednesdayWindow() deleted, past date raises, tiktokIsDraft removed, webhook closed (403).
  • Why 2 stars: zero production runs, and the carousel 2 row still needs AL's Posting Date plus the 11 composites uploaded to PostFast.
id: lmr1cbW3emlCucZw · canonical base: d26f5ed1…22c4

N8N-4 — Error Notifier ⭐⭐⭐

N8N ON ERROR N8N → EMAIL
Wired on all 3 publishers since 28/07/2026

Emails AL whenever a publication robot errors. Before 28/07 it only watched the carousels: a shorts or quotes failure was invisible. Now all three publishers report to it, which makes the "past date = visible error" rule actually reach AL's inbox.

Technical details
  • Not wired on nwslt-welcome (robot awaiting AL's keep-or-delete decision). bridge and My workflow are dead, deletion pending AL's go.
id: QB2gHEg5a7Y9qffi

N8N-5 — Pinterest Auto-Publisher ⭐ (inactive)

N8N INACTIVE · WAVE 2 NOTION → POSTFAST
Built 28/07/2026 in a parallel session · never armed, never run

Future publisher for the Pinterest system (1 authored pin per day, boards = the 7 Quote Kinds, destination eliteoutsiders.com). Deliberately INACTIVE: AL classed Pinterest as wave 2, the boards do not exist on the real account yet and the prompt has never been rehearsed. Alignment audit against the 28/07 doctrine (closed webhook, error notifier, date read from the planner, slot in content-system) ran the same evening; see the Pinterest Grammar for the live state.

Technical details
  • Pieces: Pinterest Grammar v1.6 + Prompt v2.8 + empty Content Planner (Notion) + master HTML on this site + this workflow.
  • Blocked before any first run: create the 7 boards on the real account, profile About, rehearsal.
id: KLWzgswqM59G3hIE · webhook: /webhook/pinterest-trigger

N8N-6 — Lifestyle Auto-Publisher ⭐⭐ (inactive)

N8N INACTIVE · REHEARSAL PENDING NOTION → POSTFAST
Built 29/07/2026 on the Quotes v5 template · never run

Publisher for the lifestyle photos: reads the Lifestyle Content Planner (rows AL sets to In progress), posts the single photo with its caption to Instagram + TikTok + Facebook at the Notion Posting Date, APPROVED direct, then writes Done + Posted At. Refuses to publish a row with an empty Caption (caption prompt first) or an empty PostFast Image Key (upload first): a half-ready row is skipped with a visible warning, never posted naked.

Technical details
  • Same hard rules as the other publishers: date read from Notion, past date raises, closed webhook (shared header key), error notifier wired.
  • Deliberately INACTIVE until one supervised rehearsal on a single row. The 103-row queue (Thursdays to 2028) waits in Not started; nothing can fire by accident.
id: QRVpsd9Z3r9vBbpU · webhook: /webhook/lifestyle-trigger · planner: a32a49e3…
▶ ROUTINES2 cards · 1 local Claude Code live + 1 that does not exist

ROUTINE1 — Cron auditor Does not exist

ROUTINE NEVER RAN NOT DEPLOYED
Card kept as a specification only · marked non-existent 2026-08-27

This robot was specified but never built. Nothing audits the cron inventory automatically. Everything below is a spec, not a running system.

What was verified on 2026-08-27. The 11 Claude Code scheduled tasks on this Mac were listed one by one: quarterly API key rotation, DMARC audit, CRM brief, YouTube carousel recheck, downloads cleanup, annual admin rollover, evening checklist, Saturday quote approval, skincare check, EO quote weekly, Anima growth tracking. None of them audits crons. No Anthropic Cloud routine could be confirmed either.

The tell was on this page all along. The spec below says the robot must stamp data-last-update on every single run, even a run that finds nothing. That stamp sat at 05-05-2026 until today, while CRON14 through CRON17 were added to this page over July and August. A robot that had run even once in four months would have moved that date.

What this cost. The 2026-08-27 audit found 31 of 50 scheduled tasks running with no documentation at all. CRON10 on this page had been advertising status: SPEC, code to write since May while the robot was live. That is the gap this card was supposed to close.

Original specification (never implemented)
  • Scope 1 — crons / routines / events audit. Once a month, this robot reads code from the 3 repos (eliteoutsiders-site, eliteoutsiders-tools, eo-backup-worker) and looks for any new CRON (CF Worker schedule or local launchd plist), ROUTINE (claude.ai/code/routines or ~/.claude/scheduled-tasks/), or EVENT-triggered code (page load handlers, form submit POSTs, webhook handlers) AL built without listing on /crons. For each new entry, it adds a card in the right outer dropdown (CRONS / ROUTINES / EVENTS) on a separate git branch and opens a Pull Request for AL to validate. Like a teacher checking each new drawing is taped to the wall, leaving a sticky note for any missing.
  • Scope 1bis — TECHNICAL BLOCK update (added 2026-05-05). When a new CRON / ROUTINE / EVENT is detected (Scope 1), the robot ALSO updates the "Technical block — for Claude Code" table at the bottom of /crons : adds the new cron expression to the Crons row, the new admin endpoint to the Admin endpoints row, the new secret to the Secrets row if applicable, and any new R2/D1/KV binding to the Bindings row. Same branch + PR as Scope 1.
  • Scope 2 — infra audit (added 2026-05-03). Same pass also scans the 3 repos for any new external tool / service / binding (CF Workers, R2 buckets, KV namespaces, D1 tables, Resend, Kit, Notion, Cal.com, GCal, Stripe, Lemon Squeezy, Anthropic, OAuth providers, etc.) and any new wiring between them. Updates /infra-tree if a tool or interconnection is missing — same branch + PR mechanic as Scope 1.
  • If nothing new in either scope: silent commit of an HTML comment <!-- monthly audit YYYY-MM-DD: no new crons / no infra changes --> and exit. No PR for nothing.
  • ON EVERY RUN (even when nothing new), the robot must update BOTH data-last-update spans:
    • public/crons.html, right of the "Crons" H1
    • public/infra-tree.html, right of the "Infra Tree" H1
    With today's date in the format DD-MM-YYYY—UPDATE (e.g. 22-09-2026—UPDATE). Both date changes go into the PR (or the silent commit if no PR).
  • Intended host: an Anthropic Cloud routine under the ID trig_01UU96BG1d6FXknfdYHDEY21, running in an isolated sandbox with HTTPS clones of the 3 repos, branch + PR only, never a direct push to main. No such routine could be confirmed on 2026-08-27, and the Run now button that pointed at it has been removed from this card.
intended cron: 0 9 1 * * · status: NEVER BUILT · until it exists, this page and /infra-tree are maintained by hand, and their data-last-update stamps have to be moved by whoever edits them
Technical reference (for Claude Code)
WhatValue
Workereo-backup-worker · Cloudflare account 88ff6259a88cb5fb8e8b9e13e082d7c4
URLhttps://eo-backup-worker.alexandre-corne-ac.workers.dev
Repo~/Projects/eo-backup-worker/
Crons0 2 1 * * (events-archive) · 0 2 * * SUN (social-snapshot) · 0 3 1 * * (backup) · 0 4 * * 1 (page-flow-crawl) · 0 4 1 * * (url-audit) · 30 4 * * 1 (quotes-trigger) · 0 5 * * 1,4 (kit-sync-check) · 0 5 1 1,7 * (analytics-map-refresh) · 0 6 * * * (error-events-watchdog) · 30 6 * * * (newsletter-dispatch) · 0 7 * * * (cal-monitor) · 0 7 * * SUN (personal-ops) · 0 8 * * 1 (weekly-snapshot) · 0 9 * * 1 (content-sync-check) · 0 10 * * * (notion-freshness) · 0 21 * * SUN (cal-randomize) · */30 * * * * (gcal-scan) — 17 deployed, read back from the Cloudflare schedules API on 2026-08-27, social-snapshot added 2026-08-26. Note: 0 7 * * SUN (personal-ops) has no card on this page yet.
SecretsRESEND_API_KEY · KIT_API_KEY · D1_API_TOKEN · D1_PREVIEW_API_TOKEN · BACKUP_HEALTH_TOKEN · CAL_API_KEY · NOTIFY_FROM · NOTIFY_TO · NOTION_API_KEY · NOTION_WEEKLY_PARENT_ID · NOTION_ARCHIVE_PARENT_ID · NOTIFY_TO_WEEKLY (optional)
BindingsR2 eo-backups (+ prefix events-archive/YYYY-MM/ from CRON5 LIVE) · D1 eliteoutsiders + eliteoutsiders-preview · KV BACKUP_STATE
Admin endpoints (POST + ?token=BACKUP_HEALTH_TOKEN)/admin/run-now · /admin/cal-monitor-now · /admin/cal-randomize-now · /admin/weekly-snapshot-now · /admin/events-archive-now · /admin/page-flow-crawl-now · /admin/url-audit-now · /admin/analytics-map-refresh-now · /admin/error-events-watchdog-now · /admin/gcal-scan-now · /admin/content-sync-check-now · /admin/notion-freshness-now · /admin/kit-sync-check-now · /admin/newsletter-dispatch-now · /admin/quotes-trigger-now · /admin/ops-preprocess-now · /admin/social-snapshot-now (list re-read from src/index.js on 2026-08-26)
Healthcheck (GET + ?token=BACKUP_HEALTH_TOKEN)/backup/last-run
State KV (cal-monitor)namespace id 09082b5063a34c238de4b117efaf7d93 · key cal_slot_health:v2

ROUTINE2 — EO Quote weekly pipeline CONTENT

ROUTINE WEEKLY Fri @ 18h Vietnam
Friday 18:00 local time, Asia/Ho_Chi_Minh since 22/09/2026 · LIVE since 2026-05-23

Weekly local Claude Code routine: produce next Saturday quote (Canva visual + PostFast upload + Notion satellites X/Threads) unattended through Phase 8. AL gets a final report + manual Phase 9 PostFast approval.

Technical details
  • Scope. Every Friday evening, fetch the Quote Content Planner (Notion data source collection://9078c89a-5334-43fa-b501-7ac535d31c0d), pick the row with Posting Date = upcoming Saturday + Validation=Yes + Status="Not started", and run the full Quote pipeline single-row per Quote Grammar v1.5 and Grammar/Prompt doctrine V1.
  • Phases executed unattended. Phase 0 invariants → Phase 1 Notion query → Phase 2 live schedule fetch (content-system) → Phase 5 Canva production (single OR multi based on chars/height) → Phase 5.8 pixel check P3 → Phase 6 PostFast signed-URL upload + write keys → Phase 7 X + Threads satellites (NO LinkedIn per Grammar E.4 / AL Option A 2026-05-23) → Phase 8 Notion Status "In progress".
  • Phase 9 manual. Routine completion notifies AL (notification + open Finder folder). AL approves the 5 PostFast posts (IG/TT/FB/X/Threads) either manually in PostFast UI OR by saying "go approve Qxx" in any Claude conversation (Chrome MCP auto-approve).
  • Anti-patterns guarded. Em-dash interdiction in captions/filenames · signature_image element never edited (preserve or delete only) · single-page KEEPS signature + alexandre_corne · multi-page P1 cover strips both · multi PN final keeps both · all safe-zone/slot values fetched live from content-system.
  • Local Claude Code routine. Storage: ~/.claude/scheduled-tasks/eo-quote-weekly/SKILL.md. Runs only while a Claude Code app is open on AL's Mac; if closed, fires on next launch.
  • Manual trigger (off-cycle). Open Claude Code → Scheduled section sidebar → eo-quote-weekly → Run now. OR in any Claude conversation, say "schedule la quote de la semaine" and the same logic kicks off in-conversation.
cron: 0 18 * * 5 (local time of the Mac, Vietnam) · management: ~/.claude/scheduled-tasks/eo-quote-weekly/ · out-of-scope crons (CF Workers): /crons CRONS section above
Technical reference (for Claude Code)
WhatValue
TriggerLocal Claude Code scheduled-task eo-quote-weekly · cron 0 18 * * 5 local, Vietnam
Storage~/.claude/scheduled-tasks/eo-quote-weekly/SKILL.md
Notion DBs touchedQuote Content Planner 9078c89a-5334-43fa-b501-7ac535d31c0d (read+update) · X 81b3d7ba-d64e-40af-bd99-03627abfb9f0 (create) · Threads 90a1b900-bdc9-4691-aab9-45e6cacf0ed7 (create)
CanvaBrand template EAHIywfFr_k · folder FAHKGTx8peY · 5 slots per Grammar D.5
PostFastAPI api.postfa.st · header pf-api-key · endpoint POST /file/get-signed-upload-urls
Secrets read~/.config/eo/secrets.env: CF_ACCESS_CLIENT_ID, CF_ACCESS_CLIENT_SECRET, POSTFAST_API_KEY
Output path~/Library/Mobile Documents/com~apple~CloudDocs/iCloud Documents/DD/Elite Outsiders/content/quotes/2026/ kebab-case Qxx-title.png or -p1/-p2/...png
n8n downstreamEO Quotes Weekly workflow (reads Notion publication rows Status="In progress" + PostFast Image Key, creates SCHEDULED+PENDING_APPROVAL post in PostFast)
▶ EVENTS3 cards · user/system action triggers

EVENT1 — Past weeks cleaner

EVENT ON EVENT /alsboard load
Client-side onload · LIVE since 2026-04-30

When /alsboard loads, auto-purge weeks whose end date has passed.

Technical details
  • When you open /alsboard, it automatically removes from the display the weeks whose end date has passed — the page cleans itself.
trigger: client-side onload · file: eliteoutsiders-tools/public/alsboard.html · code: parseWeekEndDate + purgePastUpcomingWeeks · persists via PUT /api/admin/state/eisenhower_matrix_al

EVENT2 — Post-quiz mailer

EVENT ON EVENT quiz submit
On Trauma Map submit · LIVE since 2026-04-26

When user submits Trauma Map quiz, send results email via Resend (fire-and-forget).

Technical details
  • When someone finishes the Trauma Map quiz and submits their email, this robot immediately sends a Resend email with full results (Past/Current scores + Inner/Outer Journey + 2 doors). Like a postman delivering the letter within the minute. If Resend crashes, the user still got their {ok:true} — the email goes out in background via waitUntil.
  • Code: functions/api/trauma-map/save.js calls buildTraumaMapEmail + sendEmail. Fire-and-forget. No retry yet.
trigger: POST /api/trauma-map/save · file: eliteoutsiders-site/functions/api/trauma-map/save.js

EVENT3 — Coaching library

EVENT ON EVENT toolkit upload
On /me/toolkit upload · LIVE since 2026-05-01

When client uploads to /me/toolkit, store blob in R2 + index in D1 toolkit_resources.

Technical details
  • When a coaching client uploads a document from /me/toolkit (PDF, DOCX, image, audio, video — up to 100 MB), this robot stores the file in the private R2 vault eo-user-toolkit and writes the index card in D1 toolkit_resources (with columns r2_key/mime/bytes added via migration 0018). Like a librarian shelving the book on the client's private shelf and noting the location. Nobody else (besides the client and the admin) can access the file — every read re-verifies identity before serving the blob.
  • Read side: GET /api/me/toolkit/file/<id> streams the R2 blob with ownership check (WHERE user_email = <jwt-email>) and Range header support so videos can scrub. Annotation side: toolkit_annotations table (migration 0019) — 3-color highlights + margin comments, same mechanics as the manifesto reader. Annotations active only on text types (TXT, MD, DOCX rendered to HTML via mammoth.js); PDF/image/audio/video read-only for V1.
  • Pricing link: the coaching cell on /workwithalex turns green and redirects to /me/toolkit as soon as a purchases row with product='coaching' and status='paid' exists for the logged-in email. Single source of truth: /api/me.
trigger: POST /api/me/toolkit/upload · files: eliteoutsiders-site/functions/api/me/toolkit/*.js + public/me/toolkit.html · binding: R2 USER_TOOLKIT → bucket eo-user-toolkit
For Claude
Legend — categories

Cron = Cloudflare Workers cron (eo-backup-worker, scheduled handler). Deterministic, server-side, zero LLM tokens. For mechanical jobs (count rows, push Notion API).

Routine = Claude Code scheduled task (~/.claude/scheduled-tasks/<id>/SKILL.md). Spawns a Claude session. Costs tokens. For reasoning jobs (audit, brief, review).

Event = Application code fired on a user or system event (page load, form submit, file upload, webhook). Not scheduled, not LLM.

CONTENT tag = any cron / routine / event that touches the content pipeline (scripts, shorts, carousels, quotes, thumbnails, scheduling, n8n workflows, etc.). Visual flag so the content-related automation surface is auditable in 1 glance. Cards carrying this tag should mirror in /backdata/content-system.

CF Worker code: ~/Projects/eo-backup-worker/.

Conventions — non negotiable

Single AL inbox — every automated email produced by any cron, routine or event MUST land at al@eliteoutsiders.com. Never alexandre.corne.ac@gmail.com (personal inbox, off-limits to ops noise).

How it's enforced — every Resend send in eo-backup-worker reads env.NOTIFY_TO with hardcoded fallback al@eliteoutsiders.com. Audit grep: grep -rnE "to:.*[\"'].*@" src/ — every match must end with @eliteoutsiders.com in the fallback string.

Override — if a single cron ever needs a different recipient, set a per-cron secret (e.g. NOTIFY_TO_WEEKLY) via wrangler secret put on the worker. Do not change the fallback. Last audit: 2026-05-08 — 4 stale fallbacks fixed (weekly-snapshot, events-archive, url-audit, personal-ops).