← Resources · Product · Workflow
A raw list of providers is not a catalog. This is the SOP I use to turn one into the other — and to keep it correct after the tenth re-run, when the list has changed and nobody remembers what was edited by hand.
The workflow assumes one thing: a file, not a database, is the source of truth. Everything else follows from that. You edit the file, run the tool, and the catalog matches the file. No admin panel, no half-remembered manual fixes, no drift.
intake file → validate → dry-run → write → verify
(you edit) (no network) (no writes) (commits) (reads back)
Four gates before anything lands. Each one fails on a different class of mistake, so a broken row never reaches the public catalog.
One structured file — YAML or CSV — with one entry per provider. Keep it human-editable: the person curating the list is not going to write SQL.
- name: Example Legal # natural key — unique and stable
status: draft # draft = invisible; active = published
geo: [ES-CT, PT-30] # where they actually operate
services:
- goal_type: residency # slug from a fixed taxonomy
title: Residence permit support
price_from: 450
verification:
checked: 2026-07-14
by: manual
name is the natural key. Not a row id — a stable human name. Re-running the tool replaces that provider's services and geo from the file. The file wins; the database never accumulates orphans nobody can explain.status: draft by default. Nothing is public until someone flips it. Curation and publication are separate acts.goal_type comes from a fixed list. Free-text categories are how a catalog turns into 40 spellings of the same thing by month three.The cheapest gate. Runs offline, catches: duplicate keys, missing required fields, malformed dates, unknown taxonomy slugs. Run it after every edit — it costs a second and it means the next three steps are about real problems.
If validation needs a database connection, it isn't validation. It's a deploy with extra steps.
Connects, resolves taxonomy slugs against the live list, prints exactly what would change — and writes nothing:
+ create Example Legal (2 services, 1 geo)
~ update Northern Notary (services 3 → 2) ← check this
- orphan Old Partner Ltd (in DB, not in file)
The line to read carefully is the middle one. Creates are easy to see; a quiet removal — a service that fell out of the file because someone deleted a line — looks like nothing at all until it's gone from the catalog. Making the diff visible before the write is the whole point of this step.
Same command, minus the flag. It runs with elevated credentials because it bypasses the read-only rules that protect the public catalog — so it stays in your hands, never in the app's.
Not "the tool exited 0". Query the catalog through the public path — the same permissions a stranger has — and confirm:
active rows come back. A draft leaking into public results means the permission rules, not the tool, are broken.The catalog is the top of the funnel, and that only works if it's honest. Two things make the difference between a directory and a funnel:
Early on I published a count — "232 providers" — that came from a parsing run, not from the live catalog. It went onto the site, into a deck, into three docs. When the underlying data changed, the number lived on for weeks in places nobody thought to check.
The fix is a rule: a number is either read from the source at render time or it doesn't get published. A hardcoded count is a claim with an expiry date and no label.