Last month my n8n community node for Holded — the ERP used by 100,000+ Spanish SMEs — got the verified badge and became the only Holded integration on n8n.io.
A
It covers 42 resources and 323 operations of the Holded API v2, plus a trigger node for 58 webhook events.
Here's the part that matters: I hand-wrote almost none of it. This is the story of shipping a huge API surface as a solo dev by treating the node as a compiler target instead of a codebase.
Why bother with a node for a Spanish ERP?
Spanish SMEs are migrating to n8n from Make and Zapier for a very unglamorous reason: per-operation pricing. The moment you automate invoicing properly — say, an ecommerce store pushing 1,000+ orders a month into accounting — a per-task bill stops making sense. Self-hosting also plays well with GDPR anxiety.
The blocker was that n8n had zero Holded connectivity. Everyone was gluing HTTP Request nodes to a quirky API. That's the gap.
The math that forbids hand-writing
Holded's API v2 is big: invoices, contacts, products, stock, CRM funnels, projects, payroll, remittances… When I mapped it, I got 591 endpoints across 42 resources.
An n8n node describes every operation declaratively: resource options, operation options, every parameter with its type, defaults, and displayOptions controlling when it appears in the UI. Written by hand, that's maybe 60–80 lines per operation. Times 323.
That's a ~25,000-line file I would have to maintain by hand every time Holded touches their API. No.
The pipeline: scrape → spec → generate
So the repo has a spec/ folder that most n8n nodes don't:
spec/
├── scrape-v2.py # scrapes the official API docs
├── v2-endpoints.json # 72,000 lines: every endpoint, param, type
└── gen-v2.py # 888 lines that write the node for me
-
scrape-v2.pywalks Holded's API reference and dumps every endpoint — path, method, params, body schema — intov2-endpoints.json(~72k lines of structured truth). -
gen-v2.pycompiles that spec intoV2Generated.ts: 22,000 lines of n8n property descriptors — every resource, operation, and field, with correctdisplayOptionswiring. - A 173-line dispatcher routes
(resource, operation)pairs to generic HTTP execution. The actual.node.tsshell is 355 lines.
The generated file opens with the only comment that matters:
// AUTO-GENERATED by spec/gen-v2.py from spec/v2-endpoints.json.
// Do not edit by hand — regenerate with `python3 spec/gen-v2.py`.
When Holded changes their API, I re-scrape, re-generate, diff, release. Maintenance went from "career" to "afternoon".
What codegen can't do (and what I still wrote by hand)
Generation gets you coverage. It does not get you good UX. Three things stayed human:
-
The Contact resource is fully handwritten. It's the resource everyone touches first, and the generated version was technically correct but clumsy — search deserves first-class
email/vatNumber/phonefields, not a generic filter collection. High-traffic paths earn hand-polish; the long tail stays generated. - Naming and grouping. A dropdown with 42 resources is only usable if names match what users see in Holded's UI, not what the API paths say.
-
n8n's lint rules.
eslint-plugin-n8n-nodes-basehas strong opinions (alphabetized options, description formats, casing). The generator had to learn them — fixing lint output in the generator instead of in the generated file is the whole trick of this approach.
Getting the "verified" badge
n8n's verification reviews your code before the node becomes installable on n8n Cloud (community nodes otherwise need self-hosting). What it costs you: strict linting, no runtime dependencies, real docs, and review iterations measured in days-to-weeks.
What you get: distribution. Verified nodes show up in the in-app node panel for every n8n Cloud user, plus a public integration page. For a niche B2B tool, that's the difference between "GitHub repo with 12 stars" and "discoverable by every n8n user who types your product's name".
Takeaways if you're building a big-API node
- If the API has >50 operations, don't type them. Generate them. A one-day scraper plus a one-week generator beats months of typing and years of drift.
- Spec first. The intermediate JSON is the asset — the TS is disposable output. I can regenerate for a future n8n API version, or even for a different platform, from the same spec.
- Hand-write the top 10% by traffic. Generated long tail, polished hot paths.
- Teach the generator the linter's opinions. Never fix generated files by hand, not even once.
-
Webhooks are half the value. The trigger node (18 objects / 58 events:
invoice.create,stock.update, …) is what turns an ERP from "destination" into "event source".
Try it
-
n8n: Settings → Community Nodes →
@francodesystems-npm/n8n-nodes-holded(or find "Holded" directly on n8n Cloud) - Repo: github.com/francodesystems/n8n-nodes-holded (MIT)
- Docs, coverage matrix and use cases: francodesystems.com/open-source/n8n-nodes-holded
- If you're migrating between Holded API v1 and v2, I wrote up the 8 traps that will actually bite you (Spanish).
Ready-to-import workflow templates (Shopify → invoices, Stripe → paid invoices, overdue-invoice chasing…) are landing on my n8n creator profile — I'll update this post with the links as they're approved.
Questions about the codegen pipeline or the verification process — ask away in the comments. 🇪🇸
Top comments (1)
Anticipating the obvious question: "why not just feed an OpenAPI spec into a generator?"
Because there isn't one. Holded's API reference is HTML docs only — no OpenAPI/Swagger file published. That's why the pipeline starts with a scraper instead of a spec download.
Which turned out to be a blessing in disguise: building my own intermediate JSON meant I could normalize the API's inconsistencies (naming, param casing, pagination quirks) at the spec level, so the generator stays clean. If Holded ever publishes an OpenAPI spec, I only have to replace the scraper — everything downstream survives.