← Articles Harupa
2026-08-23RU EN UA BY PL

CRM on Markdown, part 2: 27 hours, 23 commits and a catalog of failures

In part one I showed how to set up a CRM on markdown files in an evening. This is how it was actually built: with numbers, a timeline and, above all, failures. The failures are not an appendix to the story — they are its skeleton: almost every part of the system appeared as a response to a specific accident, and without those accidents the design makes no sense.

Context: I was doing outreach for my video generator — looking for first users and conversations with them. Claude Code was helping. Everything below happened within 27 calendar hours on August 22–23, 2026: 23 commits, 16 contact cards, 12 documents, 307 lines of code. And every fact here is backed by a file, a commit or a verbatim quote — the "never make things up" rule extends to articles about the system too.

Why — and why at three contacts

I started the system when I had three contacts. Not thirty — three. The wording from the commit that started it all: "right now there are three live contacts in three different places (a Telegram thread, a LinkedIn profile, a voice message), and not a single place where you can see whose move it is and what has been promised."

A caveat right away: this is my one case, not a pattern. But there is logic to it. Chaos doesn't arrive at a hundred contacts — it arrives at the fourth one; you just notice it later, when there's nothing left to reconstruct the correspondence from.

The second reason runs deeper. For me, two outcomes count: a client or an insight. A conversation that taught me the pain isn't there is a result, not a failure. That's why the card has a "what we didn't expect" section and no "deal size" field. The goal defines the data schema, not the other way around — a selling CRM and a research CRM have different fields on the same skeleton.

And third, about roles. The machine collects, drafts texts and remembers everything. The human decides and sends. That line is drawn hard — and, as you'll see below, for good reason.

The design: the board was never kept by hand, not for a single day

The card, the board and the script were born in one commit. That matters more than it seems: the decision "the card's frontmatter is the source of truth, the table is derived" was made before the first table row existed. The board never had to be "migrated to generation" — it was simply never kept by hand at all.

Three design decisions I consider portable anywhere.

Two overdue thresholds, not one. My move burns after two days, theirs after five. The justification is ethical, not technical: your own debt burns faster than someone else's — people owe us no reply, but we owe them one. A single "days since last touch" column would have merged the two metrics and hidden my own inaction behind someone else's silence.

Three hypothesis states, not two. The script prints NOT ASKED as a word, not as a zero. "Not confirmed" and "not asked" are different diagnoses. It proved itself on live data almost immediately: one hypothesis got 0 confirmations out of 3 — and that turned out to be a targeting miss, the sample simply contained no one of the right type. A zero printed as a zero would have read as a refutation.

Two breeds of hypotheses — two separate fields. A product hypothesis ("their pricing is unpredictable") is aggregated across all cards. A person hypothesis ("I don't think he's the one who decides") is never aggregated and lives inside the card. Separate fields make substituting one for the other physically impossible. Why this is the single most valuable trick of the whole construction comes a bit further down, in the interpretation failures: discipline failed even the person who wrote the rule.

The line between what gets generated and what gets written by hand runs along meaning: the script writes the run date, the human writes the run's decision, as an argument string. Mechanical goes to the machine, semantic goes to the human.

The failures. Breed one: data

The system remembered something false or lost something true.

Someone else's phrase became the owner's voice — in three files. A line said by a woman I was talking to landed in my voice profile as mine. The condition that allowed it: no verbatim log of the conversation existed; the card had been assembled from a retelling. Retelling loses the author — lines without an explicit "who said this" fuse into one voice, and the fusion surfaces weeks later, when the source can no longer be recovered. Closed with the rule "verbatim, not retold" and a ⚠ callout in the corrupted files — not with deletion: silently erasing the mistake means erasing the lesson with it.

The agent invented an offer — and recorded it as mine. The field "what we offer people" acquired "benchmarks across video models" — something I had never offered anyone. The agent made it up. Closed with a table "what I can give — in my own words" with a "who I've said it to" column: an offer without a source is a showcase of wishful thinking, not a fact.

A contact without an address is lost for good. The profile link wasn't captured in the same sitting, and the feed that had surfaced the person doesn't reproduce: the same query ten minutes later returned different people. Two targeted attempts to find them — nothing. "I'll add it later," in a system fed from a non-deterministic feed, means "I'll lose it."

Breed two: process

There was a rule — and it didn't work.

A ready-made text went out under a stranger's post. The most expensive miss: a comment prepared for the most valuable entry point in the funnel was posted under another person's post — a person marked "do NOT contact", no less. The mechanism is an honest one, and it isn't about carelessness: the card held a link to a profile, while a comment gets posted under a post. Those are different addresses, and the difference only shows at the moment of sending. Hence the guard: if the next step is a send and there is no exact address, the run fails and the board lights up with NO ADDRESS.

The first version of that guard lied. It matched on the word root "reply" and fired on a card saying "waiting for a reply to two questions" — which is a waiting state, no address needed there. The conclusion, which went straight into a code comment: a false positive is more dangerous than a miss — a warning that lies teaches people to ignore it.

My corrections weren't recorded anywhere. Product decisions went into one file, the voice profile into another, facts into cards. But corrections of the "how to talk to me" kind went nowhere and stayed in the chat — and the same mistakes kept repeating. It surfaced through a direct question: "how do you record my feedback, do you take it into account?" The answer was "I don't," and within the hour a corrections log existed. If a category of knowledge has no file, the knowledge lives in the conversation and dies with the session — or the employee.

The process state lived nowhere. Each contact's state was stored; the state of the cycle itself was not. The question "if I open a new session tomorrow, will it understand the week isn't over yet?" had the answer "it won't." That's how the run log appeared: a process has state just like a record does, and it needs a table of its own.

A rule cancelled a text, and the text stayed. A message draft, rejected by the new rules, sat in a card for three days as if still current, and the "next step" field kept saying "send it." It was found only by a full inventory sweep. A rule that cancels an artifact must cancel the artifact itself — otherwise the ban and the banned object live peacefully in the same file.

Breed three: interpretation

A wrong conclusion drawn from correct data. The most expensive breed.

A user's silence was explained by the product. The man was moving house. From the trail in production — paid, keeps a balance, six charges within two minutes, then silence — I deduced: motivation is there, money is there, access is there, so it must be the product stopping him. The conclusion was labeled the cleanest case in the sample. The explanation turned out to be mundane: a move. Reversed two minutes after a line from the owner of the knowledge — that is, from me, who had learned about the move from life, not from the data.

The mechanism here deserves a full dissection. My own insights file says it in plain words: production behavior explains the "what", not the "why". I broke my own rule the very day I wrote it down. A behavioral trail without human context lies confidently and convincingly — which is exactly why the split between "person hypothesis" and "product hypothesis" is structural, enforced by separate fields, not by discipline. Discipline didn't hold even for the rule's author.

A conclusion rewritten silently — twice in one day. A silently corrected conclusion looks, a month later, as if it had never been revised at all, and the cost of the mistake vanishes along with the lesson. Now a collision is recorded as an entry: what was claimed, what came in against it, and what exactly settled the dispute.

The questionnaire was collecting confirmation of ourselves. The first version of the survey: 1-to-5 scales over our own list of pains. A person picks from our list, we get a confirmation of ourselves and call it research. Caught before sending — and we were about to send it through someone else's network, meaning the price of the mistake was someone else's reputation. The answer options are your hypotheses; an aggregate over them measures you, not the market.

A 9-out-of-9 pain nearly became an offer. Nine out of nine confirmed the problem — and three of the nine had already solved it for free with a local tool and were reporting success, not pain. A confirmation counter without an "already solved" flag sells other people's success as your demand.

Four reversals of my own decisions

Articles about "how I built X" usually leave this out, yet for the reader it's worth more than the wins.

What was reversedHow soonWhat refuted it
The conclusion "the product is stopping him"2 minutesa fact from the man's life: he was moving
"In disputes, the owner has priority"45 minutesthe owner himself: "my goal is effectiveness, not attempts to please me"
Declining a separate search skill7 hoursthe criterion was wrong: skills split not by frequency but by the question a person asks
An edit to the search query stringone runmeasurement: it was 1 out of 3, it became 0 out of 3

The last row is a lesson of its own. I was fixing the wrong defect, and the fix made things worse. That came to light only because the miss had been recorded: without the record I would have fixed the same thing all over again. Recording a negative result is the improvement mechanism; a fix without a measured "before" is indistinguishable from ritual.

Add to this the arithmetic that forbids A/B testing at my volumes: the formula n = 16σ²/δ² at a 20% baseline response rate and a desired lift of 10 percentage points demands 256 contacts per variant. At forty to sixty, only a twofold effect is detectable. An important caveat: this entire quantitative base comes from other people's channels — B2B email and American cold calling; I'm porting the mechanisms, not the numbers. Instead of A/B — rounds of five conversations on a single wording, with the decision threshold declared in advance.

A rule without a guard does not survive

The main takeaway of the whole story, and it was earned, not invented.

Every time a rule was written down as prose, it got broken. Including cases where the one breaking it was the same person who had written it — in the same file, two paragraphs below. The only rules that survived were the ones backed by a script that fails the run. Five of the eight guards are a direct response to an accident that had already happened; not one was written "for the future, just in case."

A guard, in turn, gets verified by mutation: you break a card on purpose — the run must go red on exactly that card; you restore it — it must go green. A green test that cannot be made red is a decoration. An honest number from the original: I mutation-tested three guards out of eight; the rest lived on trust. In the published version of md-crm that debt is paid — all the guards are covered by 24 automated tests, and the test suite itself is mutation-verified: disable a guard, and exactly its tests go red.

And a final touch that proved the rule on the tool itself. The only bug that silently corrupted data lived in the only place in the script not covered by a test: a magic offset of "+24" against a real separator length of 22 characters. The first run went down a different code path and looked healthy, while every subsequent one cut two characters into the previous journal line. Over eight iterations the script kept growing checks for the cards — and not a single check of itself.

What here is settings, and what is skeleton

The skeleton: one file per person, state in the frontmatter, the board derived, guards that fail the run, a verbatim journal, cycle state kept separately from contact state. This ports anywhere — hiring included: an ATS differs from a CRM in the vocabulary of its stages, not in architecture.

The settings: the 2- and 5-day thresholds, the checkpoints at 5/12/25 conversations, the hypothesis codes. My values are reference points with their sources cited, not constants; everyone will pick their own.

Setting all of this up for yourself takes two commands — the instructions are in part one. The catalog of failures, anonymized, ships with the skill as a references/failures.md file: it seems right to me that a tool should carry the history of its own accidents with it.