September 2026

How I Branch AI Skills for SaaSant Journal Entries

Every month I post the same short list of journal entries. A line-of-credit paydown. A line-of-credit advance. A batch of processor deposits. Each one is structurally identical — read a source file, produce a SaaSant import CSV — and each one hits completely different accounts in a completely different direction.

I’m a controller. I build this tooling for my own close, and I run it on real entries every month. What follows is the structure I landed on after the obvious approaches failed, including the part most write-ups leave out: what my validation script does not check.

The approach that doesn’t work

The instinct is to write one skill called “journal entries” and teach it every case. It works beautifully for the first entry type. It starts to wobble on the second. By the third, the instructions are arguing with each other.

The failure is specific: a paydown debits the loan and credits cash. An advance does the exact opposite. Those two rules live four paragraphs apart in the same document, and the model has to decide which one applies before it does anything useful. When it guesses wrong, the output is a perfectly balanced entry posted backwards — which is the worst possible failure, because nothing downstream will flag it.

Splitting into one standalone skill per entry type fails differently. The account mapping is the only thing that genuinely differs. Everything else — the output column contract, the sign convention, the batching rule, the file naming — is identical, so you end up maintaining five copies of the same output format and discovering the drift between them at the worst moment.

Parent and branches

What works is treating it as one skill with a routing layer. The parent owns everything that is true for every entry. A branch owns only what makes that entry different.

journal-entry-saasant/
├── SKILL.md              — router, output contract, invariants
├── validate.py           — one validator, all branches
├── references/
│   ├── _branch-template.md
│   ├── loc-paydown.md    — BRANCH
│   ├── loc-advance.md    — BRANCH
│   └── deposit-batch.md  — BRANCH
└── examples/             — known-good input/output pairs

The parent SKILL.md carries a routing table and the invariants that no branch is allowed to override: the SaaSant column contract, the sign convention, how entries are grouped into batches, and the rule that certain fields are always left blank. A branch file is short. It says when it applies, which accounts it touches, in which direction, and what must be true about its own output.

The test for whether something belongs in a branch is narrow: does it change which account gets debited? If not, it belongs in the parent. Almost everything belongs in the parent.

Make the model declare its route

The single highest-value line in the framework is a formatting rule. Before the model produces any output, it must state which branch it took and why:

Branch: loc-paydown. Basis: source file shows a
principal reduction against the credit line.

This costs nothing and does three things. It forces the routing decision to happen explicitly instead of implicitly inside the output. It gives me a one-line explanation to check before I look at a single number — and checking a routing decision takes two seconds, where checking a full entry takes minutes. And when something is wrong, it tells me immediately whether I have a routing problem or a mapping problem, which are fixed in completely different files.

Most bad entries I’ve seen in testing were routed wrong, not calculated wrong. Surfacing the route is how you catch that class of error at a glance.

One validator, not one per branch

Every branch runs through the same validate.py. The script writes its results into the run manifest, and — this matters — the model is explicitly forbidden from authoring that block itself. A validation result the model wrote is not a validation result. It’s a claim.

What the script actually asserts today:

That’s it. That’s the whole list.

What it does not check

Here is the part I think is worth publishing, because I haven’t seen anyone else publish theirs.

My validator confirms an entry is well-formed and internally consistent. It does not confirm the entry is correct. Specifically, it does not:

Which means a structurally valid, internally balanced file with the wrong amount or the wrong account passes my validator cleanly. A balanced wrong entry is still wrong.

Debits-equal-credits is nearly free to implement and nearly worthless as a guarantee of correctness. It catches arithmetic slips and format drift. It does not catch the errors that actually cost money. I still review every entry before posting, and the validator’s job is to make that review shorter, not to replace it.

I’m writing that down publicly for a reason. An automation that reports “12 issues found” implies everything else was verified. That implication is how people get comfortable posting things nobody checked. The honest version names the population it examined and the population it skipped — and right now mine skips a lot.

What I’d copy if I were starting today

Four things, in order of how much they’ve paid off:

None of this requires new software. It’s a folder, a routing table, and a Python script that either passes or doesn’t.

Next up, I’m wiring the tie-out check — comparing the entry back to the source statement — because that’s the one that moves this from “well-formed” to “demonstrably right.” I’ll write up what breaks.

If you’re building something similar and want to compare notes on what your checks miss, I’d genuinely like to hear it. That’s the conversation worth having — not which model you used.

← Back to Blog