On Wednesday, I needed something mundane: a list of board members in a number of Danish companies. Not for a database. Not for a product. Just for an afternoon of lead research — who sits where, and which chairmen overlap with people I'm already talking to.
That kind of data is public. It's on datacvr.virk.dk. Every single company in Denmark has a page. It should take twenty minutes.
It didn't. And it's worth telling because it illustrates a very common tension in practical work with data and AI: the official way is often the right way — but what do you do while it's being set up?
The Official Ways
For programmatic access to CVR data, there are two canonical ways:
-
Datafordeler.dk — the state's own distribution channel. Complete, free, maintained by the Danish Business Authority. Requires MitID Erhverv or email-based setup with IP whitelisting. Realistic timeframe: 1-3 business days for approval plus an evening for the actual integration.
-
Cvrapi.dk premium — commercial API on top of the same data. Premium token requires email contact. Also several days.
Both are the right choices in the long run. Neither solves my task today.
The Open Page is Already There
While I waited, I manually opened a CVR page in the browser and looked. The board is listed there. The management is listed there. The chairman is marked. Each person is a clickable link to their own page, where all their board positions are listed.
All the data I need is in the HTML. The only obstacle is Cloudflare's bot protection, which returns 403 to anything that smells of a script.
curl: blocked. http get in nushell: blocked. WebFetch from my AI tool: blocked.
It's reasonable from virk.dk's side — they don't want automated traffic straining their servers. But it also means that a very common task has to be solved through a real browser.
The Plugin That Made the Difference
nu_plugin_browse is a small nushell plugin written by Tyarel8. It exposes one new command: http browse. Behind the scenes, it opens Chrome (headless or visible), waits for a number of seconds, and returns the fully rendered HTML.
The interesting flag is --with-head. It opens a visible Chrome window instead of a headless one. Cloudflare's challenge system recognizes headless browsers as suspicious, but lets real browser windows through.
let html = (http browse --with-head --wait 12sec
"https://datacvr.virk.dk/enhed/virksomhed/47458714")
A 12-second window opens. The Cloudflare challenge runs, the JavaScript application renders, and I get 422KB of HTML back. Inside it lies Lego's complete circle of people.
From HTML to Structured Table
HTML is not a good format to work with. It needs to be transformed into something I can filter, sort, and cross-reference. This is where nushell's own strength comes in: parse --regex with named groups, and a small state machine in a for loop.
The idea is to find three kinds of "events" in document order:
- a category heading (Management, Board, Supervisory Board)
- a role in parentheses (Chairman, Vice-Chairman, Director)
- a person link with a name
Once you've captured them, you iterate linearly through the list, keeping track of the current category and a pending role. Every time a person appears, you emit a row with category + role + name.
The result is a regular nushell table:
| category | role | name |
|---|---|---|
| Management | Director | Jesper Andersen |
| Board | Chairman | Carsten Rasmussen |
| Board | Vice-Chairman | Poul Hartvig Nielsen |
| Board | Trine Christensen | |
| Board | Substitute | Sidsel Numan Andersen |
Which can be sorted, filtered, joined with other tables, and piped to anything. Total time from task to working tool: about an hour and a half, including finding the plugin.
What I'm Not Saying
I'm not saying that scraping is better than the official API. It's not. This solution has clear weaknesses:
- It opens a visible browser window per lookup — not good for batch processing
- It's slow (10-15 seconds per company)
- It's fragile — if virk.dk changes its HTML layout, the parser will break
- It must respect virk.dk's resources — it's not a tool for fetching all Danish boards at night
The Datafordeler setup is running in parallel. When it's ready, I'll switch the backend. The interface itself — cvr virk 47458714 — can remain the same.
The Deeper Principle
This is a good example of what I call compound systems: a workflow consists of multiple components, and the right choice for each component depends on what you need right now, not what is the officially correct answer in its general form.
The components in my little tool are:
- a headless browser (Chrome) to interact with a protected website
- a plugin (nu_plugin_browse) that makes it accessible from the shell
- nushell itself for orchestration, parsing, and structuring
- a small regex-based state machine to convert HTML to a table
- a pipe architecture so the output can be used with anything else in my toolkit
None of the components are smart on their own. Together, they turn a data source that was otherwise practically inaccessible into a regular nushell table.
It's precisely the same architectural pattern as compound AI systems: small, deterministic, well-defined parts that individually solve a part of the problem, and which together become something usable. Generative AI is one component you can throw into such systems, but not necessarily the whole answer.
What It Means for SMEs
Next time your team is faced with a data task where "we're waiting for access from IT/the vendor/the authority," ask yourself:
- Is the data publicly or semi-publicly available somewhere you can access it now?
- Is there a pragmatic way that delivers 80% of the value in 5% of the time, while the official way is being set up?
- What is the cost of waiting versus having a temporary solution that will break in three months?
The answer isn't always scraping. It could be a spreadsheet. It could be a CSV file sent manually once a week. It could be opening 20 websites manually the first time and automating it the second time.
But if the answer is always "we're waiting for the official integration," there's often half a year's work that just doesn't get done. That kind of cost is hard to see on a spreadsheet — but it's real.
Finally
Thanks to Tyarel8 for nu_plugin_browse. It's a small plugin with a big impact — precisely the kind of tool that makes the difference between "we're waiting for access" and "we have a working prototype tool ready."
The entire solution is available as a nushell module in my CVR toolkit. When the Datafordeler credentials arrive, I'll switch the backend. Until then: cvr virk 47458714 returns Lego's board in 12 seconds. That's enough for me to move on to what I was actually supposed to do.