Huginn
Personal homelab automation bot and dashboard — a TypeScript monolith with a typed event bus, multi-mode scheduler, and 23 service adapters that glues together the entire homelab.
highlights
- Grew from a COVID vaccine slot notifier to a full homelab dashboard over five years
- Typed event bus with SQLite persistence and content-hash dedup across 23 service adapters
- 15 scheduled runners — from DynDNS rotation to LLM-powered email parsing that auto-builds Hevy workout routines
- Attention banner surfaces persistent, actionable alerts requiring human review before action
- Dropped all Google Cloud in 2024 — fully self-hosted on Komodo + Gitea Actions
Overview
Huginn is a personal homelab automation bot and dashboard — a TypeScript monolith running Express and React with SQLite, backed by a multi-mode scheduler, a typed event bus, and adapters for 23 external services. It lives on the homelab server and handles everything from rotating DynDNS records to parsing the powerlifting coach’s emails and building Hevy workout routines autonomously.
Named after one of Odin’s ravens — the one that thinks.
Five Years of Huginn
- 2021-03Initial commit — COVID vaccine slot notifier (plain JS)
- 2021-08Discord bot merged in · Firestore for message persistence
- 2022-10TypeScript rewrite · Docker replaces pm2
- 2023-05ScheduledRunner abstraction introduced · Cubs game schedule runner
- 2023-10Porkbun DynDNS — migrated from Google Domains when it was killed
- 2024-06Google Cloud dropped entirely · ESM · SQLite · Gitea Actions CI/CD
- 2026-04v6.0.0 — React dashboard, tile system, sidebar navigation
- 2026-06Integration explosion — arr stack, Hevy, event-discovery
- 2026-0723 adapters · 22 tiles · 15 runners
Architecture
The backend is a single Express process. The scheduler owns all automation; the API layer is thin REST. The frontend is a Vite SPA, proxied through Express in dev and served as static files in production.
huginn/
├── src/
│ ├── scheduler.ts ← loads runners, dispatches by trigger type
│ ├── scheduled/ ← runners (one file or colocated directory each)
│ ├── events/ ← typed EventMap, dedup+persist bus, poll/webhook sources
│ ├── adapters/ ← one file per external service
│ ├── dashboard/tiles/ ← tile loaders (backend half)
│ └── routes/ ← REST API, auth, webhooks
└── client/src/
├── tiles/ ← React tile components (auto-discovered)
└── runners/ ← React runner detail views (auto-discovered)
Scheduler — runners declare one of three trigger modes: frequency (interval), schedule (cron via croner), or on (event-driven). The scheduler dispatches accordingly; runners don’t need to know which mode they’re running in.
Event bus — content-hashed and SQLite-persisted. Sources emit typed events; runners subscribe via on: [{ event, filter }]. Event-driven runners handle both "event" and "manual" trigger kinds — the latter replays the last stored payload when “Run Now” is pressed in the UI, which makes testing and one-off re-runs straightforward.
Runner state — per-runner SQLite rows via a RunnerStore. Merge-patch writes, no schema to maintain.
Attention banner — surfaces persistent, actionable alerts on the dashboard homepage. Major container updates that can’t auto-deploy, pending Overseerr approvals, unmapped exercises blocking routine application. Each item can carry action buttons that dispatch directly to runner action functions.
Selected Runners
coach-program
The powerlifting coach sends workout programs via Truecoach — an app with no useful API. The coach’s emails land in Gmail; this runner subscribes to gmail:message.received, parses the email body with an LLM, maps exercises to Hevy’s exercise library, and auto-creates routines via the Hevy API. Unmapped exercises surface as attention banner items so they can be resolved manually before the routine is applied.
event-discovery
Finds local events based on personal preferences and creates Google Calendar entries for them. Runs on a schedule, queries a local LLM for event research and synthesis, and syncs results to a dedicated calendar — making it easier to find weekend plans without manually scanning event sites.
container-updates
Polls WUD (What’s Up Docker) for new image tags. Minor and patch updates are automatically deployed for containers managed through Komodo — patches the image tag in the compose file, calls UpdateStack + DeployStack. Major updates create an attention banner item requiring manual review. Kuma maintenance windows are toggled around deployments to suppress false-positive uptime alerts.
qbittorrent-stall-guard
Detects stalled or suspiciously silent torrents. Uses a decide() pure function to pick between restart, blocklist-remove, and noop — keeping the decision logic testable without live state. If Prowlarr health indicates the VPN is down, it escalates the signal rather than blindly restarting.
planka-to-gcal
Event-driven off planka:card.due-set. When a due date is set on a Planka card, it creates or patches a corresponding Google Calendar event. Canonical example of the event-driven runner plus manual-replay pattern.
learnings
- A typed event bus and runner abstraction scales from "poll a thing" to "LLM pipeline" without redesign
- Dropping cloud dependencies (Firestore → SQLite, Discord → ntfy) reduced ops surface significantly
- Building the dashboard forced clarity on what useful automation actually means vs. what's just clever