new Date(<unparseable>) yields an Invalid Date whose toISOString()
throws RangeError, and normalizeSearchItem runs inside an unguarded
items.map(), so one malformed RSS item killed the entire search with
{"error": "Invalid Date", "code": "API_ERROR"} and exit 1. The
un-CDATA'd fallback capture in parseRssItems can deliver exactly such a
value. An unparseable pubDate now degrades to the same shape as an
absent one (posted "", date null); every other item survives. Three
new cases pin the malformed shapes, each failing on the unfixed code.
jobbank-cli
CLI for Akademikernes Jobbank — Denmark's job portal for highly educated candidates.
Data sources:
- RSS feed:
https://jobbank.dk/job/rss?{params}— 100 items max, all search filters work - Job detail:
https://jobbank.dk/job/{id}/— JSON-LD (Schema.org JobPosting) embedded in page HTML
Authentication: None required. A browser User-Agent header is sent, but Jobbank may still block automated requests with Cloudflare bot protection. In that case the CLI exits with a clear error and callers should use a WebSearch fallback rather than retrying. Format: RSS XML (search), HTML with embedded JSON-LD (detail).
Installation
cd skills/jobbank-search/cli
bun install
Commands
| Command | Description |
|---|---|
search |
Search job listings via RSS feed |
detail |
Full detail for a single job posting |
All commands accept --format json|table|plain (default: json).
All errors are written to stderr as { "error": "...", "code": "..." } and the process exits with code 1.
Filter Reference Tables
Job Types (--type / cvtype)
| Code | Label |
|---|---|
| 3 | Fuldtidsjob |
| 6 | Graduate/trainee |
| 13 | Deltidsjob |
| 8 | Vikariat |
| 12 | Ph.d. & Postdoc |
| 11 | Freelance |
| 15 | Iværksætterprojekt |
| 14 | Event |
| 9 | Praktikplads |
| 4 | Studiejob |
| 5 | Studieprojekt/speciale |
Location / Region (--location / amt)
| Code | Label |
|---|---|
| 2 | Storkøbenhavn |
| 3 | Nordsjælland |
| 14 | Østsjælland |
| 4 | Vestsjælland |
| 5 | Sydsjælland & Øer |
| 13 | Fyn |
| 12 | Sønderjylland |
| 11 | Sydvestjylland |
| 9 | Vestjylland |
| 10 | Sydøstjylland |
| 7 | Midtjylland |
| 8 | Østjylland (Aarhus) |
| 6 | Nordjylland |
| 20 | Bornholm |
| 21 | Øresundsregionen |
| 22 | Grønland & Færøerne |
| 23 | Udlandet (Sverige) |
| 24 | Udlandet (Norge) |
| 19 | Udlandet (øvrige) |
Work Area / Function (--work-area / erf)
| Code | Label |
|---|---|
| 20 | Administration |
| 38 | Arkitektur & Design |
| 22 | Bank & Forsikring |
| 43 | Data & Analyse |
| 47 | Eksport |
| 41 | Forskning & Udvikling |
| 28 | Human Resources |
| 49 | Indkøb |
| 34 | Internet & Multimedia |
| 32 | IT - Hardware |
| 33 | IT - Netværk & Telekomm. |
| 31 | IT - Software |
| 24 | Jura |
| 35 | Kommunikation, Media & SoMe |
| 46 | Konstruktion & Beregning |
| 37 | Kunst & Kultur |
| 26 | Ledelse & Planlægning |
| 29 | Marketing & Reklame |
| 40 | Medicinal & Sundhed |
| 45 | Naturvidenskab |
| 23 | Organisation & Forening |
| 52 | Politik & Samfund |
| 44 | Produktion |
| 27 | Projektledelse |
| 50 | Rådgivning & Support |
| 30 | Salg |
| 39 | Socialvæsen |
| 42 | Teknik |
| 25 | Topledelse |
| 48 | Transport & Logistik |
| 36 | Undervisning |
| 21 | Økonomi & Forvaltning |
Education Field (--education / udd)
| Code | Label |
|---|---|
| 20 | Administration |
| 43 | Anlæg, Byggeri & Konstruktion |
| 29 | Arkitektur, Kunst & Design |
| 47 | Elektro & Telekommunikation |
| 32 | Fødevarer & Veterinær |
| 38 | Human Resources |
| 28 | Humaniora |
| 24 | IT |
| 23 | Jura |
| 44 | Kemi, Biotek & Materialer |
| 45 | Klima, Miljø & Energi |
| 37 | Landbrug & Natur |
| 22 | Marketing & Business |
| 48 | Maskin & Design |
| 46 | Matematik, Fysik & Nano |
| 31 | Medicinal & Sundhed |
| 30 | Naturvidenskab |
| 41 | Organisation & Ledelse |
| 35 | Produktion, Logistik & Transport |
| 34 | Samfundsvidenskab |
| 25 | Sprog, Media & Kommunikation |
| 33 | Teknik & Teknologi |
| 26 | Undervisning & Pædagogik |
| 21 | Økonomi & Revision |
Industry (--industry / branche)
| Code | Label |
|---|---|
| 10359 | Advokat & Revision |
| 11669 | Byggeri & Anlæg |
| 11634 | Elektronik & Maskin |
| 16791 | Fagforeninger, A-kasser & Pensionskasser |
| 10358 | Finans, Forsikring & Pension |
| 10442 | Forskning & Uddannelse |
| 15407 | Fødevarer & Dagligvarer |
| 10364 | Handel & Service |
| 10331 | IT & Tele |
| 17209 | Klima, Energi & Forsyning |
| 16826 | Kommuner |
| 10341 | Kultur, Medier & Underholdning |
| 10333 | Medicinal, Biotek & Kemi |
| 10363 | Papir, Møbel & Materialer |
| 11626 | Regioner, Sundhed- & Socialvæsen |
| 15586 | Rådgivning & Konsulentservice |
| 10362 | Stat, Politik & Samfund |
| 10440 | Transport |
| 12450 | Vikar & Rekruttering |
Remote Work (--remote / fjernarbejde)
| Value | Label |
|---|---|
helt |
Fully remote |
delvist |
Partially remote |
Suitable For (--suitable-for / andet)
| Code | Label |
|---|---|
| 2 | Nyuddannede |
| 4 | International baggrund |
| 5 | Erfarne |
search — Search job listings
Endpoint: GET https://jobbank.dk/job/rss?{params}
bun run src/cli.ts search [flags]
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--key |
string | — | Keyword search (title, company, keyword) |
--exclude |
string | — | Exclude keywords (antikey) |
--type |
number | — | Job type code (cvtype). Repeatable for multiple: --type 3 --type 6 |
--education |
number | — | Education field code (udd). Repeatable. |
--location |
number | — | Region code (amt). Repeatable. |
--work-area |
number | — | Work area / function code (erf). Repeatable. |
--industry |
number | — | Industry code (branche). Repeatable. |
--suitable-for |
number | — | Suitable-for code (andet). Repeatable. |
--company |
number | — | Company ID (virk) |
--remote |
string | — | Remote work: helt or delvist |
--since |
string | — | Posted on or after date, format YYYY-MM-DD (oprettet) |
--limit |
number | — | Cap total results returned by CLI (client-side) |
--format |
string | json |
Output format: json, table, plain |
Important limitation: The RSS feed returns a maximum of 100 items per request. There is no pagination via RSS — the
page=parameter has no effect on the RSS endpoint. If your query matches more than 100 jobs, only the first 100 are returned. Themeta.totalfield reflects the true total count (fetched separately from the HTML search page title), whileresultsis capped at 100.
Multi-value flags: Flags marked "Repeatable" map to params that accept multiple values in the API (repeated query params). Pass them multiple times:
--type 3 --type 6sendscvtype=3&cvtype=6.
RSS Parsing
The CLI fetches the RSS feed and parses each <item> as follows:
- id: extracted from the URL path —
/job/{id}/{company-slug}/{title-slug}— the first numeric segment after/job/ - title: from
<title>(CDATA) - description, company, location, jobType, deadline: parsed from the
<description>field, which has the format:"JobType hos Company, Location (Ansøgningsfrist: DD.MM.YYYY)"or"JobType hos Company, Location (Ansøgningsfrist: løbende)" - url: from
<link> - posted: from
<pubDate>, normalized to ISO 8601
Example
bun run src/cli.ts search --key python --location 2 --type 3 --limit 10
bun run src/cli.ts search --key "data scientist" --remote helt
bun run src/cli.ts search --industry 10331 --work-area 31 --format table
bun run src/cli.ts search --education 24 --suitable-for 2 --since 2026-03-01
Response shape
{
"meta": {
"total": 457
},
"results": [
{
"id": "1234567",
"title": "Senior Data Scientist",
"company": "Novo Nordisk",
"location": "Bagsværd",
"jobType": "Fuldtidsjob",
"description": "Fuldtidsjob hos Novo Nordisk, Bagsværd (Ansøgningsfrist: 12.04.2026)",
"url": "https://jobbank.dk/job/1234567/novo-nordisk/senior-data-scientist",
"posted": "2026-03-02T00:00:00+01:00",
"date": "2026-03-02",
"deadline": "2026-04-12"
}
]
}
Field details
| Field | Type | Notes |
|---|---|---|
id |
string | Numeric job ID extracted from URL |
title |
string | Job title |
company |
string | Company name, parsed from description |
location |
string | Location string, parsed from description |
jobType |
string | Employment type (e.g. "Fuldtidsjob", "Graduate/trainee"), parsed from description |
description |
string | Raw RSS description field (single-line summary) |
url |
string | Full URL to job posting |
posted |
string | Publication date in ISO 8601 |
date |
string | null | Publication date as YYYY-MM-DD (derived from posted), or null if absent |
deadline |
string | null | Application deadline as YYYY-MM-DD (converted from the feed's DD.MM.YYYY), or null if "løbende" / not present |
meta.totalis fetched from the HTML page<title>in a secondary request (pattern:"{N} relevante job og karriereopslag"). If the secondary request fails,meta.totalisnull.
detail — Full job detail
Endpoint: GET https://jobbank.dk/job/{id}/
bun run src/cli.ts detail <id> [--format json|plain]
The id is the numeric job ID from search results (the id field). The short URL https://jobbank.dk/job/{id}/ redirects to the full slug URL and returns HTTP 200.
The CLI fetches the HTML page and extracts the <script type="application/ld+json"> block containing a Schema.org JobPosting object.
Example
bun run src/cli.ts detail 1234567
bun run src/cli.ts detail 1234567 --format plain
Response shape
{
"id": "1234567",
"url": "https://jobbank.dk/job/1234567/",
"title": "Senior Data Scientist",
"description": "<p>Full HTML description of the role...</p>",
"datePosted": "2026-03-02",
"deadline": "2026-04-12",
"employmentType": ["FULL_TIME"],
"company": {
"name": "Novo Nordisk",
"logo": "https://jobbank.dk/images/dynamic/company/logo/12345/"
},
"location": {
"streetAddress": "",
"city": "Bagsværd",
"postalCode": "",
"country": "DK"
}
}
Field details
| Field | Type | Notes |
|---|---|---|
id |
string | Numeric job ID (from identifier.value in JSON-LD) |
url |
string | Canonical URL of the job posting |
title |
string | Job title |
description |
string | Full HTML job description body |
datePosted |
string | Publication date in ISO 8601 format (YYYY-MM-DD) |
deadline |
string | null | Application deadline (validThrough in JSON-LD), ISO 8601, or null if absent |
employmentType |
string[] | Schema.org employment type values, e.g. ["FULL_TIME"] |
company.name |
string | Hiring organization name |
company.logo |
string | null | URL to company logo, or null if absent |
location.streetAddress |
string | Street address (may be empty) |
location.city |
string | City (may be empty for international jobs) |
location.postalCode |
string | Postal code (may be empty) |
location.country |
string | Country code (may be empty) |
locationfields may be empty strings for international jobs or postings that do not specify a physical location.
Error handling
All errors are written to stderr in JSON format and exit with code 1:
{ "error": "Job not found", "code": "NOT_FOUND" }
{ "error": "Jobbank is blocking automated requests with Cloudflare bot protection. Skip this portal or use the WebSearch fallback.", "code": "API_ERROR" }
{ "error": "No JSON-LD found on job page", "code": "PARSE_ERROR" }
{ "error": "--key or at least one filter is required", "code": "MISSING_REQUIRED" }
Implementation notes
User-Agent
All HTTP requests include a browser User-Agent header:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
This is not guaranteed to bypass Cloudflare bot protection. If Jobbank returns a Cloudflare challenge page, the CLI reports that condition and callers should skip the portal or use a WebSearch fallback.
RSS description parsing
The RSS <description> field follows this pattern:
Fuldtidsjob, Graduate/trainee hos Novo Nordisk, Bagsværd (Ansøgningsfrist: 12.04.2026)
Fuldtidsjob hos DTU, Lyngby (Ansøgningsfrist: løbende)
Parse strategy:
- Split on
hos— left side is job type(s), right side isCompany, Location (Ansøgningsfrist: Deadline) - From the right side, extract the parenthetical
(Ansøgningsfrist: ...)for deadline - Remaining text is
Company, Location— split on first,to separate company from location
JSON-LD extraction
On detail pages, find <script type="application/ld+json"> containing "@type": "JobPosting" and parse the JSON. Map fields:
identifier.value→idurl→urltitle→titledescription→description(HTML)datePosted→datePostedvalidThrough→deadline(may be absent →null)employmentType→employmentType(array)hiringOrganization.name→company.namehiringOrganization.logo→company.logojobLocation.address.streetAddress→location.streetAddressjobLocation.address.addressLocality→location.cityjobLocation.address.postalCode→location.postalCodejobLocation.address.addressCountry→location.country
Rate limiting
No explicit rate limits are enforced, but Cloudflare is present. Add a 300–500ms delay between sequential requests (e.g. when fetching total count from HTML in addition to the RSS feed). The search command makes at most 2 requests (RSS + HTML for total count).