Building with AI

The Personal Resource Graph

My automation estate hit 40+ scheduled jobs, 315 database tables, two chat listeners, and a fleet of local models โ€” and started rotting silently. So I had my agents build one typed graph of everything that runs. In its first hour it caught a job that had been failing every 15 minutes for days.

Cam Fortin · August 2026 · 14 min read · ๐ŸŽง audio below
๐ŸŽง
Prefer to listen? Audio edition (~5 min)
Made with Picturebook from this post · Intro · 1/6

In The Iceberg I wrote about why personal AI tools die: the demo is the tip, the product is the plumbing. This post is about what happens when the plumbing wins โ€” when you've shipped so many working systems that the plumbing itself becomes the thing that can kill you.

Here's what my estate looked like last week: forty-plus scheduled jobs across a Mac Studio, Anthropic's cloud, and Railway. Two chat listeners โ€” Telegram and iMessage โ€” each spawning full agent sessions per message. 315 database tables in 28 prefix families on one shared Postgres. Local Ollama models doing real classification work. Canonical markdown files mirrored into shared Apple Notes. A dozen web surfaces reading all of it. Every piece worked when it shipped.

And then, over one week, entropy collected its rent:

  • Every interval-based scheduler on the machine went dormant simultaneously โ€” jobs loaded, enabled, apparently healthy, silently not firing. Twelve hours of nothing before anyone noticed. Then it happened again the next day.
  • The Telegram listener โ€” my main way of talking to the whole system โ€” died at 9:48pm and stayed dead for 15 hours, while its own status file cheerfully reported running.
  • A queue that both chat listeners write into was being drained by a script that, it turns out, had never existed. The listeners had been telling me to run it by name for weeks.
  • A notification job had been failing every 15 minutes for days โ€” a macOS permissions quirk โ€” and nothing watched it, so nothing said so.

None of these were bugs in any one system. They were failures of the relationships between systems: producers without consumers, watchers that nobody watched, promises in prose that no code kept. You cannot grep for that class of problem. You need a map.

One graph of everything that runs

So we built one โ€” the Personal Resource Graph: a single typed, directional graph of every automation, datastore, surface, service, credential, host, spec document, and local model in the estate. Not a diagram. A build artifact, generated from inventory sources, validated by invariants, rendered as a live page, and enforced by CI.

315
nodes
526
typed edges
9
node types
14
edge types

The ontology is stolen from people who solved this at company scale, then aggressively shrunk. From Backstage: the catalog shape โ€” but no teams, no domains, no org chart, because there's one owner. From Dagster and OpenLineage: assets as the unit of truth and freshness policies โ€” but no run history, because logs already hold that. From C4: level-of-detail discipline โ€” an automation is atomic; I don't model its internals.

Nine node types: automation, asset (a table family or canonical file โ€” never per-table), surface, service (local models are services with class: model), credential, host, doc (prompts are code here โ€” editing a spec is a behavior change), person (humans a change can break), system. Fourteen edge types with direction semantics: produces, consumes, triggers, spawns (the token-burn edge โ€” first-class in an agentic estate), mirrors_to (with a named sync job), runs_on, depends_on, authenticates_with, governed_by, notifies, exposes, shared_with, registered_in, part_of.

The graph page renders it live with saved questions as one-click states: everything that touches Apple Notes. Everything that spawns a headless agent. The blast radius of the Telegram listener. Everything leaning on one credential file. What runs where. Each answers in a click what used to take an afternoon of archaeology.

What the first build caught

Three inventory agents swept the schedulers, the repos, and the database in parallel; a merge step canonicalized their outputs under the schema. The very first build โ€” before any philosophy โ€” paid rent:

  • The 15-minute failure loop. A job exiting 127 every fire for days: the scheduler's shell can't read the folder the script lives in (macOS TCC), so it died before its first line. Manual runs worked, which is exactly why nobody caught it. Fixed with the estate's existing permission-wrapper pattern โ€” which the graph itself documented, because another job had solved the same problem months ago.
  • A 19-day orphan process polling a job file that nothing had written to since the day it started.
  • Six database prefix families no repo references โ€” ~60 tables of trip planners and game platforms whose owning apps are gone. Flagged for review, not dropped: the graph proposes, the human disposes on data.
  • Secrets in wrong places โ€” a bot token inlined, world-readable, in two scheduler configs while every other consumer read the canonical env file โ€” and ~15 doc-vs-reality contradictions, including three documents holding three different opinions about whether one legacy credential was alive.

But the clearest way to show what the graph changed is to show the graph changing โ€” before/afters drawn from the real thing:

Who watches the watchers?

Before โ€” six jobs outside the watchtower
health watchdoghealth watchdog watched jobwatched jobwatched jobwatched job7 watched jobs 7 watched jobs requests job โ€” failing 4ร—/hrrequests job โ€” failing 4ร—/hr deal watchdog โ€” dormant 60hdeal watchdog โ€” dormant 60h podcast sweep โ€” dormant 60hpodcast sweep โ€” dormant 60h iMessage listener โ€” unwatchediMessage listener โ€” unwatched
The health system was real โ€” and partial. Six schedulable things lived outside it, including the job failing every 15 minutes and a watchdog that had itself been dead for 60 hours. Watchers nobody watched.
After โ€” total coverage + a reality feed
health watchdoghealth watchdog watched jobwatched jobwatched jobwatched jobwatched job requests โ€” fixed + watchedrequests โ€” fixed + watched watched job revived + heartbeatingrevived + heartbeating watched job launchctl (reality)launchctl (reality)
Every job now carries a registered_in edge to the watchdog, silent-when-quiet scripts got heartbeats so quiet โ‰  dead โ€” and the amber edge is the kicker: the watchdog diffs the graph against the OS's live job list hourly. A job that skips registration pages my phone.

The events pipeline that was fiction

Before โ€” the queue with no drainer
telegram listenertelegram listener imessage listenerimessage listener calendar queuecalendar queue drainer: never existed drainer: never existed
Two producers, zero consumers. The listeners even named the drain script in their replies โ€” it was fiction. Invariant I3 ("every asset needs a producer AND a consumer") catches this shape mechanically.
After โ€” contract satisfied
telegram listenertelegram listener imessage listenerimessage listener calendar queuecalendar queue drainer (15 min)drainer (15 min) calendarcalendar summary pingsummary ping
The drainer now exists, runs on a 15-minute grid with a cheap empty-queue preflight, heartbeats to the health watchdog, and carries a declared freshness contract the hourly check verifies. Texting "family calendar: dinner saturday 6" now actually puts dinner on the calendar.

Three shared notes, three different lies

My canonical files mirror into shared Apple Notes โ€” a shopping list my wife and I both edit, a to-do list, a meeting agenda shared with a collaborator. Same feature, built three times, months apart. The graph forced one question the code never had to answer: who keeps each pair honest, and in which direction?

Before โ€” undeclared sync semantics
sync? which way wins? sync? which way wins? push only return path: does not exist shopping-list.mdshopping-list.md todo.mdtodo.md agenda.mdagenda.md ๐ŸŽ shared shopping note๐ŸŽ shared shopping note ๐ŸŽ to-do note๐ŸŽ to-do note ๐ŸŽ shared agenda note๐ŸŽ shared agenda note
Three mirrors, three unwritten rules. The nastiest one: the agenda sync was push-only โ€” anything my collaborator typed directly into our shared note was silently destroyed on the next push. Nothing in any repo said so.
After โ€” every mirror has a named custodian
sync_job: shopping ยท push=file wins, pull=note wins sync_job: sync-todo-note ยท push|pull sync_job: sync-agenda-note ยท โš  PUSH-ONLY (flagged) invariant I5: a mirror without a live sync_job fails the build shopping-list.mdshopping-list.md todo.mdtodo.md agenda.mdagenda.md ๐ŸŽ shared shopping note๐ŸŽ shared shopping note ๐ŸŽ to-do note๐ŸŽ to-do note ๐ŸŽ shared agenda note๐ŸŽ shared agenda note
Every mirrors_to edge must name its sync job and direction rule, or the build fails. The push-only gap didn't get quietly fixed โ€” it got visible, which is the honest first step: now it's a flagged decision instead of a data-loss surprise waiting for a human.

The model registry โ€” the sleeper feature

The addition I initially almost skipped turned out to be the one with the most obvious compounding value: local models are first-class nodes, and every pipeline that uses one carries a depends_on edge annotated with per-task accuracy.

My estate runs a small fleet: one model handles natural-language selection (photo picks, shopping-list matching) and the high-accuracy enrichment attributes (95% on category classification); a smaller sibling handles the cheaper attributes โ€” including one sitting at 48.8% measured accuracy, the weakest assignment in the fleet. That number now lives on the edge itself.

Which means two chronic problems become queries:

  • Benchmark-driven swaps. When a new model drops and benches well, "everything touching a local model" is a one-click view โ€” and it's not just a list, it's a migration checklist ranked by weakest current assignment. The 48.8% attribute is first in line, automatically.
  • Provably-safe deletion. The build found a 17GB model pulled eight days earlier with zero code references. Before the graph, deleting a model was an act of faith. Now absence of edges is evidence โ€” if everything that does real work is registered, an unreferenced model is just disk space.
Before โ€” models as invisible infrastructure
photo pickerphoto picker shopping matchershopping matcher enrichment ร—9 attrsenrichment ร—9 attrs audioโ†’tab engineaudioโ†’tab engine gemma e4bgemma e4b gemma e2bgemma e2b audio stackaudio stack ?? gemma 26b โ€” 17GB. used byโ€ฆ anything? ( no edges โ€” who uses what, and how well, was tribal knowledge )
Which pipeline leaned on which model โ€” and how accurately โ€” lived in scattered code and memory. Deleting or swapping anything meant grepping and praying.
After โ€” the swap map, with the receipts
category 95% ยท regulatory 86% b2b/b2c 48.8% โ† first swap target photo pickerphoto picker shopping matchershopping matcher enrichment ร—9 attrsenrichment ร—9 attrs audioโ†’tab engineaudioโ†’tab engine gemma e4bgemma e4b gemma e2bgemma e2b audio stackaudio stack 00 gemma 26b: zero edges = provably deletable
Every model dependency is an edge carrying its measured accuracy. The 48.8% assignment self-nominates as the swap target the moment a better model benches โ€” and the 17GB stranger's empty neighborhood is a deletion proof, not a guess.

Freshness contracts โ€” recency as data

The highest signal-per-effort idea in the whole design, and one that crossed over from an architecture problem I've been working professionally: consumers declare tolerance once; staleness becomes arithmetic.

Every consumes edge can carry a freshness_req โ€” the maximum staleness that consumer can tolerate before it's wrong. The school-lunch reply tolerates 36 hours. The pool-hours page tolerates nine days. The revenue-moves widget tolerates thirty. Because asset nodes encode real file paths, an hourly job compares each declared tolerance against the actual mtime on disk. No "is everything fresh?" vibes โ€” a contract table and a subtraction.

This inverts the usual staleness design, which puts decay guesses on the producer side. Producers don't know how their data gets used; consumers know exactly how stale is too stale. And the same inversion answers the expensive question in any agent-refresh architecture: when do you re-fetch, and when can you trust cache? Answer: refresh when a demanded contract is violated; reuse any piece that still satisfies the tightest applicable tolerance. Cache by contract, not by hunch โ€” and the cache decision is auditable, because the tolerance and the timestamp are both data.

The first contract check immediately caught three consumers whose producers weren't registered at all. That's the pattern of this whole project: every new check finds something on its first run.

Derive more, curate less

A registry dies the way wikis die: someone has to update it, and someone doesn't. My own estate contained the cautionary tale โ€” a prose "automations registry" document that drifted from the machine registry that actually alerts, because neither generated the other.

The countermeasure is structural: derive from reality wherever reality is parseable. A deterministic extractor now mechanically parses every scheduler config and the scripts they run โ€” labels, schedules, credential-file reads, headless-agent spawns, which model gets pinned โ€” and regenerates that layer of the graph on every build. Those facts are marked authoritative: a schedule change on disk corrects the graph within the hour, no language model and no human involved. LLM sweeps still matter, but only for what code can't say mechanically โ€” the semantic edges โ€” and they re-run on a slow cadence.

Enforcement, because rules aren't

The uncomfortable truth about instruction files: agents follow them most of the time. Most isn't a registry. So enforcement is layered, and the layers get progressively harder to skip:

LayerMechanismCan it be skipped?
InstructionsHard rule in every agent entry point โ€” global config, app-builder playbook, the chat listener's own child promptSometimes
Pre-commit hookRebuilds the graph when sources change; blocks the commit on violationsOnly deliberately
CIInvariants + graph/source consistency on every push, from any machineNo
Hourly reality-diffRe-extracts from configs, rebuilds, diffs the graph against the live scheduler job list โ€” anything running unregistered pages my phoneNo

That last row is the one that matters. Registration completeness isn't checked against what the graph says โ€” it's checked against what the operating system says is actually loaded. A new job that skips registration doesn't rot quietly; it generates a page within the hour. The registry can't drift from reality, because reality is the input.

The scoreboard

134
flags at first build
12
resolved same day
0
fragile interval timers left
0
unwatched jobs left

Every fix is recorded as a resolution against the flag it closes, and the pre-fix graph is frozen as a snapshot โ€” so before/after is always showable, and the remaining open flags are an honest work queue (mostly review-items on orphaned data, which the graph flags but never deletes: git is reversible, dropped tables aren't).

What I deliberately didn't build

The failure mode of this genre is the map becoming a second codebase. The ontology has a written do-not-model list: no run/execution history (logs hold that), no per-table or per-file nodes (families and canonical files only), no intra-automation task DAGs, no retry/SLA machinery (that's the runner's job), no org-chart layers, and no merging with the semantic memory graph โ€” "what do I know" and "what do I run" are different stores answering different questions. And no workflow orchestrator: at estate scale, the graph gives you the observability an orchestrator mostly sells, without the platform tax.

The one line

A complex functioning system doesn't have to be complex to operate โ€” if every relationship is explicit, the system can tell you what to tweak and what breaks downstream. Entropy doesn't announce itself. The graph makes silence impossible.

Graphify your own estate โ€” the prompt

A few people have asked how to do this to their own "personal OS" without reverse-engineering my whole setup. You don't need my code โ€” you need my shape, and an agent that can read your machine. Paste the prompt below into Claude Code (or any agent with shell + file access) at the root of wherever your automations live. It runs the same play this post describes: inventory reality, build a typed graph from it, define invariants, then wire up enforcement so the graph can't rot.

You are going to map and then govern my entire personal automation
estate ("personal OS"). Work in phases; show me the results of each
phase before starting the next.

PHASE 1 โ€” INVENTORY (read-only; fan out subagents if you can):
Sweep everything that runs, stores, syncs, or displays:
- Schedulers: crontab, launchd (~/Library/LaunchAgents +
  /Library/LaunchDaemons), systemd user units, GitHub Actions cron
  workflows, cloud scheduled jobs, webhooks.
- Long-running processes and chat/message listeners โ€” especially
  anything that spawns agent sessions (that's the token-burn edge).
- Datastores: databases (group tables into prefix FAMILIES, never
  per-table), canonical files, and anything mirrored into another
  surface (notes apps, calendars, spreadsheets).
- Surfaces: web apps, dashboards, pages that read the data.
- Services and local models (Ollama etc.) + which pipelines call
  each one, with measured accuracy where it exists.
- Credentials: env files, token files, keychain refs โ€” and every
  consumer of each.
- Docs/specs/prompt files that define agent behavior. Editing a
  spec IS a behavior change; they're code here.

PHASE 2 โ€” BUILD THE GRAPH (a build artifact, never hand-edited):
One typed, directional graph: graph/graph.json generated by a
build script from source inventory files.
Node types: automation, asset, surface, service, credential, host,
doc, person, system.
Edge types: produces, consumes, triggers, spawns, mirrors_to (MUST
name its sync job + direction rule), runs_on, depends_on,
authenticates_with, notifies, exposes, registered_in.
Derive from reality wherever reality is parseable: a deterministic
extractor parses scheduler configs and the scripts they run
(schedule, credential reads, agent spawns, model pins) and
regenerates that layer on every build โ€” no LLM, no human. LLM
sweeps only for semantic edges, on a slow cadence.

PHASE 3 โ€” INVARIANTS (fail the build on violations):
- An asset with a producer but no consumer, or vice versa.
- A mirrors_to edge without a named sync job + direction rule.
- An automation with no registered_in edge to a health watcher.
- A credential with zero consumers; depends_on cycles.
- Freshness contracts: consumes edges may declare freshness_req
  (max tolerable staleness); check declared tolerance against the
  actual mtime on disk. Consumers declare tolerance, not producers.

PHASE 4 โ€” ENFORCEMENT (layered; each layer harder to skip):
1. CLAUDE.md hard rule: anything a session creates or materially
   changes that PERSISTS must be registered in the graph the same
   session; before editing an existing system, check its blast
   radius in the graph first.
2. Pre-commit hook: rebuild the graph when sources change; block
   the commit on invariant violations.
3. CI: rebuild + invariants on every push, from any machine.
4. Scheduled reality-diff (hourly): diff the graph against the
   LIVE scheduler list (launchctl list / crontab -l / systemctl).
   Anything running unregistered notifies me. Give
   silent-when-clean jobs heartbeats so quiet โ‰  dead.

PHASE 5 โ€” REPORT:
List every flag the first build finds โ€” failing jobs, orphan
processes, producers without consumers, fictional consumers,
secrets outside the canonical env path, doc-vs-reality
contradictions, unreferenced models โ€” ranked by risk. Propose
fixes. Never delete data: flag it; I dispose.

Two calibration notes from running this for real. First, expect the first build to find fires โ€” mine found four in an hour, and every new invariant has caught something on its first run since. Second, the enforcement phase is the whole game: a graph without the reality-diff is just a prettier version of the wiki that rots. If you only build one layer, build layer 4.

Steal the shape

The public cousin of this idea is live on the Boise AI graph page, and the plumbing philosophy behind the whole estate is in The Iceberg โ€” including a starter repo. If you're building an agent estate of your own: start the graph before you think you need it. Mine found four fires in its first hour.