Idempotent ERPNext Seed Data for a Real SaaS Business
The most dangerous seed record is the one that does not exist. When there are no items, no cost centres, no issue types, and no subscription plans in the system, nothing forces you to think about how the business is actually structured. You can get away with it for a while. A one-person SaaS does not need a territory hierarchy to process its first payment.
But "does not need" is not the same as "would not benefit from." The moment I started putting real operational categories into ERPNext — lead sources, campaigns, support issue types, onboarding tasks — the system started asking questions I had been quietly avoiding. Which cost centre does hosting sit under? What are the actual subscription tiers? What does a support workflow look like when there is more than one kind of issue?
Those are not questions a seed script can answer. But a seed script can make them impossible to ignore.
The temptation to skip it
As a one-person operation it is easy to treat this work as unnecessary. There is no team that needs onboarding tasks. There is no finance department that needs cost centres. There is no support team that needs triaging categories. Everything fits in your head, and the head is always available.
The problem is that "fits in your head" is a fragile storage medium. It does not produce reports. It does not survive a bad week. And it does not scale to the point where you actually need it, because by then you have months of unstructured history and no baseline to compare against.
Setting up seed data forced me to commit decisions to something more durable than memory. The items and plans are not aspirational — they describe the product as it exists now. The cost centres are not elaborate — they separate hosting, services, and operations. The support issue types are not exhaustive — they cover the five categories that account for most tickets. The point is not completeness. The point is that the structure exists and the system enforces it.
What the seed data covers
I group the seed data by the kind of decision it supports:
| Seed data | Why it exists |
|---|---|
| Items and plans | So pricing and subscription flows have something real to work with |
| Lead sources and campaigns | So reporting can distinguish where enquiries came from |
| Territories and cost centres | So operational costs land in the right category |
| Support issue types | So support workflows can be triaged consistently |
| Onboarding tasks | So the expected setup path is documented, even if the team is one person |
That last row felt absurd at first. Onboarding tasks for a team of one. But writing them down revealed steps I had been doing from memory and occasionally forgetting. The onboarding checklist is really a deployment checklist in disguise.
Making it repeatable
The seed process needs to be idempotent. Understanding evolves. Plans change names, cost centres get reorganised, new issue types appear. Re-running the seed should update existing records rather than duplicate them.
I use a natural-key approach. If a record already exists by a stable business key, I update it. If it does not, I create it:
type SeedRecord struct {
Doctype string
Key string
Fields map[string]any
}
That lets me describe the intended business object without tying the seed to a database primary key.
The idempotency contract has one important boundary: seed data only owns the fields it sets. If an operator has changed a value that the seed did not originally define, the seed leaves it alone. That distinction matters because seed data should set the baseline, not overwrite decisions that belong to the person running the business.
What I would do differently
I would separate "starter" data from "demo" data earlier and more aggressively. They look similar in a seed script but serve different purposes. Starter data is operational defaults — the items and categories needed to run a real workflow. Demo data is illustrative — fake customers and invoices that make a dashboard look alive. Mixing them is how a fake invoice ends up looking like business history six months later.
I would also make the seed report more explicit about what it changed versus what it found already present. The more often the script runs, the more useful a concise diff becomes.
What changed
The part I did not expect was how much the system itself does the thinking for you. ERPNext asks for a fiscal year before you can post a journal entry. It expects cost centres before you can categorise a purchase. It wants issue types before support workflows make sense. At first that felt like overhead — forms demanding fields I had not decided on yet. In practice it was the system pushing me to make decisions I would otherwise have deferred indefinitely.
The seed script is how I meet that push halfway. Instead of filling in ERPNext's required fields reactively, one at a time, under pressure, the seed declares them up front. The fiscal year is set because the calendar needs it, not because an invoice is waiting. The cost centres exist because the business has running costs, not because an expense report threw a validation error.
ERPNext already knows what a well-run operation needs — it just cannot fill in the specifics for you. The discipline of writing those specifics down, even when you are small enough to skip it, is where meticulous accounting and organised thinking start. Not when you need them, but before.