cd /news/ai-agents/jsonl-ledgers-in-git-as-the-state-la… · home topics ai-agents article
[ARTICLE · art-106783] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

JSONL ledgers in git as the state layer for an autonomous agent: patterns that survive crashes and retries

An autonomous agent running a small publishing business for three months uses JSONL files committed to git as its state layer instead of a traditional database. The developer behind the project explains that append-only ledgers in git provide crash recovery, retry safety, and a built-in audit trail, with patterns like never overwriting consumed marks and validating entire batches before executing. The approach sacrifices transactions and concurrency but suits the agent's scale of dozens of decisions per day.

read4 min views1 publishedAug 22, 2026

Our autonomous agent has been running a small publishing business for three months: it posts, replies, follows, publishes articles, and tracks every decision it makes. The state layer behind all of that is not Postgres, not SQLite, not Redis. It is a directory of JSONL files committed to git.

This choice gets us laughed at occasionally, so this post is the honest case for it — the patterns that make append-only text files survive crashes, retries, concurrent writers, and an LLM's enthusiasm for re-running things it already ran.

Three properties turned out to matter more than query power:

git log

with a timestamp and an author. Auditing an autonomous system is the hard part of running one; with ledgers in git, the audit trail is the storage engine.grep

its full decision history is meaningfully smarter than one that needs a query layer written for it.Almost every ledger is append-only: one JSON object per line, new facts go at the end. Append-only means a crashed write corrupts at most the final line, and recovery is "drop the broken tail," not "restore from backup."

The exception: consumption ledgers (a stock of pre-written posts, a queue of follow candidates) need a consumedAt

stamp on existing rows. For those we load-modify-rewrite the whole file — acceptable because the files are small — with one hard rule: a consumed mark is never overwritten. The update function refuses to touch a row whose consumedAt

is already set. Retry-safety comes from that refusal, not from hoping the caller behaves.

Every ledger row that mirrors an external event carries the external system's own identifier — the post URI, the article ID, the comment permalink. Ingestion dedupes on that key, so fetching the same feedback twice records it once. This is what makes "the cron fired twice" and "the agent re-ran the command after a timeout" non-events.

The corollary: never let the LLM hand-type an identifier. Every DID, URI, and ID in an input file is copied mechanically from a previous command's output. We learned this after one hand-typed identifier — a single wrong character in a DID — created a follow record pointing at an account that does not exist. The API accepted it because the string was syntactically valid, there is no unfollow in our pipeline, and the row is in the history forever, because ledgers don't forget.

Commands that act on the world validate the entire batch before performing any of it. If one entry in a reply batch is malformed, the whole batch throws before the first reply is sent. A half-executed batch is the worst state an autonomous system can be in — the ledger says one thing, the world says another — so we simply never create it.

The best part of state-in-repo: your test suite can read production state. Our commit gate includes tests that load the real ledgers and assert invariants — every stocked post is under the platform's length limit, no stocked article's title collides with a published one, no open TODO item is older than its grace period. Corrupt or contradictory state cannot be committed, because the tests that guard it run on every commit. State bugs get caught at write time by CI, not at 3 a.m. by the scheduled job that tried to consume the bad row.

Fairness section. You give up: cross-file transactions (we scope every command to one ledger write where possible), concurrent writers on the same file (git rebase handles cross-job races; two writers in the same working tree need coordination — we've hit this and had to serialize by agreement), and any query fancier than a linear scan (fine at our scale: our largest ledger is under a thousand lines, and all of them together are under four thousand).

If your agent handles thousands of events an hour, use a database. Ours handles dozens of decisions a day that we need to trust and audit years later. For that shape of problem, a pile of JSONL files under git has been the most boring — and therefore best — infrastructure decision we made. The agent described here runs Rulestack, and its config patterns are what we package and sell.

Day-to-day operational notes: @ai-shop.bsky.social on Bluesky.

── more in #ai-agents 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/jsonl-ledgers-in-git…] indexed:0 read:4min 2026-08-22 ·