Initial release: AI-powered job application framework

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Job Search
2026-03-23 08:34:06 +01:00
committed by Mads Lorentzen
co-authored by Claude Opus 4.6
commit c66d599d75
73 changed files with 6539 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
---
name: jobbank-search
version: 1.0.0
description: >
Make sure to use this skill whenever the user mentions anything related to job
searching on Akademikernes Jobbank, jobbank.dk, or looking for academic or
highly educated positions in Denmark — even if they don't mention jobbank.dk
explicitly. Also invoke this skill for questions about Danish job listings,
graduate trainee positions, Ph.d. jobs, or finding work in specific industries
or regions in Denmark. Trigger phrases include:
jobbank, akademikernes jobbank, jobs denmark, academic jobs denmark, find job
denmark, highly educated jobs, graduate job denmark, trainee position denmark,
ph.d. position denmark, postdoc denmark, studiejob, fuldtidsjob, deltidsjob,
vikariat, freelance job, praktikplads, job søgning, jobsøgning, søg job,
ledige stillinger, nye jobs, it jobs denmark, engineering jobs denmark,
marketing jobs denmark, finance jobs denmark, healthcare jobs denmark,
remote job denmark, fjernarbejde, job københavn, job aarhus, job odense,
nyuddannede job, job til nyuddannede, international job denmark,
jobbank søgning, find stilling, data scientist job, software developer job,
projektleder stilling, konsulent job, data analyse job.
context: fork
allowed-tools: Bash(bun run skills/jobbank-search/cli/src/cli.ts *)
---
# Jobbank Search Skill
Search live Danish job listings from [Akademikernes Jobbank](https://jobbank.dk) — Denmark's primary job portal for highly educated candidates. No authentication needed. Uses the RSS feed for search (up to 100 results) and JSON-LD parsing for detailed job information.
## When to use this skill
Invoke this skill when the user wants to:
- Search for jobs, positions, or career opportunities in Denmark
- Find academic, graduate, trainee, Ph.d., or postdoc positions
- Look for jobs by keyword, industry, location, education background, or work function
- Find remote or hybrid positions in Denmark
- Get full details for a specific job posting on jobbank.dk
- Check what positions are available at a specific company on jobbank.dk
- Browse jobs suitable for new graduates or people with international backgrounds
## Commands
### Search jobs
```bash
bun run skills/jobbank-search/cli/src/cli.ts search [flags]
```
Key flags:
- `--key <text>` — keyword search (title, company, keyword)
- `--exclude <text>` — exclude keywords from results
- `--type <code>` — job type: `3`=Fuldtidsjob, `6`=Graduate/trainee, `13`=Deltidsjob, `8`=Vikariat, `12`=Ph.d. & Postdoc, `11`=Freelance, `9`=Praktikplads, `4`=Studiejob (repeatable)
- `--location <code>` — region: `2`=Storkøbenhavn, `8`=Østjylland (Aarhus), `7`=Midtjylland, `6`=Nordjylland, `13`=Fyn (repeatable)
- `--work-area <code>` — function: `31`=IT-Software, `43`=Data & Analyse, `26`=Ledelse, `29`=Marketing (repeatable)
- `--industry <code>` — sector: `10331`=IT & Tele, `10442`=Forskning & Uddannelse, `10358`=Finans (repeatable)
- `--education <code>` — education field: `24`=IT, `21`=Økonomi & Revision, `34`=Samfundsvidenskab (repeatable)
- `--remote <value>``helt` (fully remote) or `delvist` (partially remote)
- `--suitable-for <code>``2`=Nyuddannede, `4`=International baggrund, `5`=Erfarne
- `--company <id>` — filter by company ID
- `--since <YYYY-MM-DD>` — jobs posted on or after this date
- `--limit <n>` — cap results returned by CLI
- `--format json|table|plain`
> **RSS limitation:** The RSS feed returns max 100 items per request. No pagination is available via RSS. `meta.total` shows the true count; `results` is capped at 100.
### Full job detail
```bash
bun run skills/jobbank-search/cli/src/cli.ts detail <id> [--format json|plain]
```
`id` is the numeric job ID from `search` results. Fetches the job page and parses the embedded Schema.org `JobPosting` JSON-LD for structured data.
---
## How to use effectively
**Start with `search`, then use `detail` for full description.**
1. Use `search` with `--key` and/or filters to find matching jobs with IDs
2. Call `detail <id>` to get the full HTML job description, exact deadline, and company details
**Use repeatable flags for multi-value filters.** Most filter flags can be repeated to match any of the values:
```bash
# IT or Finance industry, Copenhagen or Aarhus
bun run skills/jobbank-search/cli/src/cli.ts search \
--industry 10331 --industry 10358 \
--location 2 --location 8
```
**Filter codes are documented in the README** at `skills/jobbank-search/cli/README.md`.
---
## Usage examples
### Find data scientist jobs in Copenhagen
```bash
bun run skills/jobbank-search/cli/src/cli.ts search \
--key "data scientist" \
--location 2 \
--format table
```
### Graduate trainee positions for new graduates
```bash
bun run skills/jobbank-search/cli/src/cli.ts search \
--type 6 \
--suitable-for 2 \
--format table
```
### Remote IT software jobs
```bash
bun run skills/jobbank-search/cli/src/cli.ts search \
--work-area 31 \
--remote helt \
--format table
```
### Ph.d. and postdoc positions in research
```bash
bun run skills/jobbank-search/cli/src/cli.ts search \
--type 12 \
--industry 10442 \
--format table
```
### Recent full-time jobs posted since March 1
```bash
bun run skills/jobbank-search/cli/src/cli.ts search \
--type 3 \
--since 2026-03-01 \
--format table
```
### Full details for a specific job
```bash
bun run skills/jobbank-search/cli/src/cli.ts detail 1234567 --format plain
```
### IT jobs in Aarhus or Copenhagen
```bash
bun run skills/jobbank-search/cli/src/cli.ts search \
--key developer \
--location 2 --location 8 \
--work-area 31 \
--format table
```
---
## Output formats
| Format | Best for |
|--------|----------|
| `json` | Default — programmatic use, passing IDs to `detail` |
| `table` | Quick human-readable list of results |
| `plain` | Single-job detail views (`detail` command) |
All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and the process exits with code `1`.
---
## Notes
- Data is from the public jobbank.dk RSS feed and HTML pages — no credentials required.
- RSS feed returns max 100 results per query. For higher counts, `meta.total` shows the true total.
- The `detail` command fetches a full job page and extracts the JSON-LD structured data block.
- `location` values are region codes (e.g. `2` = Storkøbenhavn), not city names.
- All filter codes are documented in `skills/jobbank-search/cli/README.md`.
+396
View File
@@ -0,0 +1,396 @@
# jobbank-cli
CLI for [Akademikernes Jobbank](https://jobbank.dk) — 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 required to bypass bot protection.
**Format**: RSS XML (search), HTML with embedded JSON-LD (detail).
---
## Installation
```bash
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}`
```bash
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. The `meta.total` field reflects the true total count (fetched separately from the HTML search page title), while `results` is 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 6` sends `cvtype=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
```bash
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
```json
{
"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",
"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 |
| `deadline` | string \| null | Application deadline as `DD.MM.YYYY` string, or `null` if "løbende" / not present |
> `meta.total` is fetched from the HTML page `<title>` in a secondary request (pattern: `"{N} relevante job og karriereopslag"`). If the secondary request fails, `meta.total` is `null`.
---
## `detail` — Full job detail
**Endpoint**: `GET https://jobbank.dk/job/{id}/`
```bash
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
```bash
bun run src/cli.ts detail 1234567
bun run src/cli.ts detail 1234567 --format plain
```
### Response shape
```json
{
"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) |
> `location` fields 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`:
```json
{ "error": "Job not found", "code": "NOT_FOUND" }
{ "error": "Failed to fetch RSS feed: 403 Forbidden", "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 must include a browser User-Agent header. Without it, Jobbank routes traffic through a bot protection layer that returns invalid responses:
```
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
```
### 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:
1. Split on ` hos ` — left side is job type(s), right side is `Company, Location (Ansøgningsfrist: Deadline)`
2. From the right side, extract the parenthetical `(Ansøgningsfrist: ...)` for deadline
3. 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``id`
- `url``url`
- `title``title`
- `description``description` (HTML)
- `datePosted``datePosted`
- `validThrough``deadline` (may be absent → `null`)
- `employmentType``employmentType` (array)
- `hiringOrganization.name``company.name`
- `hiringOrganization.logo``company.logo`
- `jobLocation.address.streetAddress``location.streetAddress`
- `jobLocation.address.addressLocality``location.city`
- `jobLocation.address.postalCode``location.postalCode`
- `jobLocation.address.addressCountry``location.country`
### Rate limiting
No explicit rate limits are enforced, but Cloudflare is present. Add a 300500ms 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).
@@ -0,0 +1,25 @@
{
"name": "jobbank-cli",
"version": "1.0.0",
"description": "CLI for Akademikernes Jobbank (jobbank.dk) — job search for highly educated candidates",
"type": "module",
"main": "src/cli.ts",
"bin": {
"jobbank": "src/cli.ts"
},
"scripts": {
"start": "bun run src/cli.ts",
"test": "bun test --timeout 30000",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@bunli/core": "latest",
"@bunli/utils": "latest",
"node-html-parser": "^6.1.13",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.4.0"
}
}
@@ -0,0 +1,14 @@
import { createCLI } from "@bunli/core"
import { search } from "./commands/search.js"
import { detail } from "./commands/detail.js"
const cli = await createCLI({
name: "jobbank-cli",
version: "1.0.0",
description: "CLI for Akademikernes Jobbank (jobbank.dk) — job search for highly educated candidates",
})
cli.command(search)
cli.command(detail)
await cli.run()
@@ -0,0 +1,154 @@
export const BASE_URL = "https://jobbank.dk"
export const USER_AGENT =
"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"
export function writeError(error: string, code: string): void {
process.stderr.write(JSON.stringify({ error, code }) + "\n")
}
export async function fetchWithUA(url: string): Promise<Response> {
const maxRetries = 6
let delay = 500
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, {
headers: { "User-Agent": USER_AGENT },
})
if (response.status === 429 || response.status >= 500) {
if (attempt === maxRetries) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`)
}
const jitter = Math.floor(Math.random() * 500)
await new Promise((resolve) => setTimeout(resolve, delay + jitter))
delay = Math.min(delay * 2, 5000)
continue
}
return response
}
throw new Error("Request failed after max retries")
}
export interface RssItem {
title: string
description: string
link: string
pubDate: string
}
function extractCdata(xml: string, tag: string): string {
// Try CDATA first
const cdataRe = new RegExp(`<${tag}><\\!\\[CDATA\\[(.*?)\\]\\]><\\/${tag}>`, "s")
const cdataMatch = xml.match(cdataRe)
if (cdataMatch) return cdataMatch[1].trim()
// Plain content
const plainRe = new RegExp(`<${tag}>(.*?)<\\/${tag}>`, "s")
const plainMatch = xml.match(plainRe)
return plainMatch ? plainMatch[1].trim() : ""
}
function extractLink(xml: string): string {
// <link> in RSS can conflict with atom namespace — extract text node after <link>
// Try CDATA variant first
const cdataMatch = xml.match(/<link><!\[CDATA\[(.*?)\]\]><\/link>/s)
if (cdataMatch) return cdataMatch[1].trim()
// Plain link
const plainMatch = xml.match(/<link>(.*?)<\/link>/s)
if (plainMatch) return plainMatch[1].trim()
// Some RSS feeds put the URL as text after <link> without a closing tag (self-closing style)
// Try matching href in atom:link
return ""
}
function parseRssItems(xml: string): RssItem[] {
const items: RssItem[] = []
// Split on <item> boundaries
const itemMatches = xml.matchAll(/<item>([\s\S]*?)<\/item>/g)
for (const match of itemMatches) {
const itemXml = match[1]
const title = extractCdata(itemXml, "title")
const description = extractCdata(itemXml, "description")
const link = extractLink(itemXml)
const pubDate = extractCdata(itemXml, "pubDate") || itemXml.match(/<pubDate>(.*?)<\/pubDate>/)?.[1]?.trim() || ""
items.push({ title, description, link, pubDate })
}
return items
}
export async function rssFetch(params: Record<string, string | string[]>): Promise<RssItem[]> {
const searchParams = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
for (const v of value) {
searchParams.append(key, v)
}
} else {
searchParams.append(key, value)
}
}
const url = `${BASE_URL}/job/rss?${searchParams.toString()}`
const response = await fetchWithUA(url)
if (!response.ok) {
throw new Error(`Failed to fetch RSS feed: ${response.status} ${response.statusText}`)
}
const xml = await response.text()
return parseRssItems(xml)
}
export interface ParsedDescription {
jobType: string
company: string
location: string
deadline: string | null
}
export function parseRssDescription(desc: string): ParsedDescription {
// Format: "JobType hos Company, Location (Ansøgningsfrist: DD.MM.YYYY)"
// or: "JobType hos Company, Location (Ansøgningsfrist: løbende)"
// or multiple types: "Fuldtidsjob, Graduate/trainee hos Company, Location (Ansøgningsfrist: ...)"
let jobType = ""
let company = ""
let location = ""
let deadline: string | null = null
const hosIdx = desc.indexOf(" hos ")
if (hosIdx === -1) {
// Can't parse — return desc as company
return { jobType: "", company: desc, location: "", deadline: null }
}
jobType = desc.substring(0, hosIdx).trim()
let rest = desc.substring(hosIdx + 5) // skip " hos "
// Extract deadline from parenthetical at the end
const deadlineMatch = rest.match(/\(Ans[øo]gningsfrist:\s*(.*?)\)\s*$/)
if (deadlineMatch) {
const deadlineStr = deadlineMatch[1].trim()
if (deadlineStr.toLowerCase() === "løbende" || deadlineStr.toLowerCase() === "lobende") {
deadline = null
} else {
deadline = deadlineStr
}
// Remove the deadline portion from rest
rest = rest.substring(0, deadlineMatch.index).trim()
}
// rest is now "Company, Location"
// Split on first ", " to get company and location
const firstComma = rest.indexOf(", ")
if (firstComma !== -1) {
company = rest.substring(0, firstComma).trim()
location = rest.substring(firstComma + 2).trim()
} else {
company = rest.trim()
location = ""
}
return { jobType, company, location, deadline }
}
export function extractJobIdFromUrl(url: string): string {
// URL format: https://jobbank.dk/job/{id}/{company-slug}/{title-slug}
const match = url.match(/\/job\/(\d+)\//)
return match ? match[1] : ""
}
@@ -0,0 +1,39 @@
import { join } from "path";
const CLI_PATH = join(import.meta.dir, "../src/cli.ts");
export interface CLIResult {
stdout: string;
stderr: string;
exitCode: number;
}
export async function runCLI(args: string[]): Promise<CLIResult> {
const proc = Bun.spawn(["bun", "run", CLI_PATH, ...args], {
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
}
export function parseJSON<T = unknown>(result: CLIResult): T {
if (result.exitCode !== 0) {
throw new Error(
`CLI exited with code ${result.exitCode}. stderr: ${result.stderr}`
);
}
try {
return JSON.parse(result.stdout) as T;
} catch {
throw new Error(
`Failed to parse JSON. stdout: ${result.stdout}\nstderr: ${result.stderr}`
);
}
}
@@ -0,0 +1,327 @@
# Jobbank.dk URL Reference
Complete reference for constructing search URLs on Akademikernes Jobbank.
## Base URL
```
https://jobbank.dk/job/
```
## Full URL Pattern
```
https://jobbank.dk/job/?key={search}&antikey={exclude}&cvtype={type_id}&udd={edu_id}&amt={location_id}&erf={workarea_id}&branche={industry_id}&andet={suitability_id}&virk={company}&fjernarbejde={remote}&oprettet={YYYY-MM-DD}&page={num}
```
**Multiple values:** Repeat the parameter name
```
cvtype=3&cvtype=13&amt=2&amt=3
```
## Query Parameters
| Parameter | Description | Format |
|-----------|-------------|--------|
| `key` | Search keywords | URL-encoded string (use `+` for spaces) |
| `antikey` | Exclude keywords | URL-encoded string |
| `cvtype` | Job type ID | Numeric ID (see table below) |
| `udd` | Education area ID | Numeric ID (see table below) |
| `amt` | Location/region ID | Numeric ID (see table below) |
| `erf` | Work area ID | Numeric ID (see table below) |
| `branche` | Industry ID | Numeric ID (see table below) |
| `andet` | Suitability ID | Numeric ID (see table below) |
| `virk` | Company name | URL-encoded string |
| `fjernarbejde` | Remote work | `helt` (fully) or `delvist` (partially) |
| `oprettet` | Posted since date | `YYYY-MM-DD` format |
| `page` | Page number | Integer (1-indexed) |
## Filter Tables
### Job Type (cvtype)
| Label | ID |
|-------|-----|
| Fuldtidsjob (Full-time) | 3 |
| Graduate/trainee | 6 |
| Deltidsjob (Part-time) | 13 |
| Vikariat (Temporary) | 8 |
| Ph.d. & Postdoc | 12 |
| Freelance | 11 |
| Iværksætterprojekt (Entrepreneurship) | 15 |
| Event | 14 |
| Praktikplads (Internship) | 9 |
| Studiejob (Student job) | 4 |
| Studieprojekt/speciale (Study project/thesis) | 5 |
### Education Area (udd)
| Label | ID |
|-------|-----|
| Administration | 20 |
| Anlæg/Byggeri/Konstruktion (Construction) | 43 |
| Arkitektur/Kunst/Design (Architecture/Art/Design) | 29 |
| Elektro/Telekommunikation (Electronics/Telecom) | 47 |
| Fødevarer/Veterinær (Food/Veterinary) | 32 |
| Human Resources | 38 |
| Humaniora (Humanities) | 28 |
| IT | 24 |
| Jura (Law) | 23 |
| Kemi/Biotek/Materialer (Chemistry/Biotech/Materials) | 44 |
| Klima/Miljø/Energi (Climate/Environment/Energy) | 45 |
| Landbrug/Natur (Agriculture/Nature) | 37 |
| Marketing/Business | 22 |
| Maskin/Design (Mechanical/Design) | 48 |
| Matematik/Fysik/Nano (Math/Physics/Nano) | 46 |
| Medicinal/Sundhed (Medicine/Health) | 31 |
| Naturvidenskab (Natural Sciences) | 30 |
| Organisation/Ledelse (Organization/Management) | 41 |
| Produktion/Logistik/Transport (Production/Logistics/Transport) | 35 |
| Samfundsvidenskab (Social Sciences) | 34 |
| Sprog/Media/Kommunikation (Language/Media/Communication) | 25 |
| Teknik/Teknologi (Engineering/Technology) | 33 |
| Undervisning/Pædagogik (Education/Pedagogy) | 26 |
| Økonomi/Revision (Economics/Accounting) | 21 |
### Location/Region (amt)
| Label | ID |
|-------|-----|
| Storkøbenhavn (Greater Copenhagen) | 2 |
| Nordsjælland (North Zealand) | 3 |
| Østsjælland (East Zealand) | 14 |
| Vestsjælland (West Zealand) | 4 |
| Sydsjælland & Øer (South Zealand & Islands) | 5 |
| Fyn (Funen) | 13 |
| Sønderjylland (South Jutland) | 12 |
| Sydvestjylland (Esbjerg) | 11 |
| Vestjylland (West Jutland) | 9 |
| Sydøstjylland (Southeast Jutland) | 10 |
| Midtjylland (Central Jutland) | 7 |
| Østjylland (Aarhus) | 8 |
| Nordjylland (North Jutland) | 6 |
| Bornholm | 20 |
| Øresundsregionen (Øresund Region) | 21 |
| Grønland & Færøerne (Greenland & Faroe Islands) | 22 |
| Udlandet - Sverige (Abroad - Sweden) | 23 |
| Udlandet - Norge (Abroad - Norway) | 24 |
| Udlandet - øvrige (Abroad - other) | 19 |
### Work Area (erf)
| Label | ID |
|-------|-----|
| Administration | 20 |
| Arkitektur/Design (Architecture/Design) | 38 |
| Bank/Forsikring (Banking/Insurance) | 22 |
| Data/Analyse (Data/Analysis) | 43 |
| Eksport (Export) | 47 |
| Forskning/Udvikling (Research/Development) | 41 |
| Human Resources | 28 |
| Indkøb (Procurement) | 49 |
| Internet/Multimedia | 34 |
| IT-Hardware | 32 |
| IT-Netværk/Telekomm. (IT-Network/Telecom) | 33 |
| IT-Software | 31 |
| Jura (Law) | 24 |
| Kommunikation/Media/SoMe (Communication/Media/Social Media) | 35 |
| Konstruktion/Beregning (Construction/Calculation) | 46 |
| Kunst/Kultur (Art/Culture) | 37 |
| Ledelse/Planlægning (Management/Planning) | 26 |
| Marketing/Reklame (Marketing/Advertising) | 29 |
| Medicinal/Sundhed (Medicine/Health) | 40 |
| Naturvidenskab (Natural Sciences) | 45 |
| Organisation/Forening (Organization/Association) | 23 |
| Politik/Samfund (Politics/Society) | 52 |
| Produktion (Production) | 44 |
| Projektledelse (Project Management) | 27 |
| Rådgivning/Support (Consulting/Support) | 50 |
| Salg (Sales) | 30 |
| Socialvæsen (Social Services) | 39 |
| Teknik (Engineering) | 42 |
| Topledelse (Executive Management) | 25 |
| Transport/Logistik (Transport/Logistics) | 48 |
| Undervisning (Education) | 36 |
| Økonomi/Forvaltning (Finance/Administration) | 21 |
### Industry (branche)
| Label | ID |
|-------|-----|
| Advokat/Revision (Law/Accounting) | 10359 |
| Byggeri/Anlæg (Construction) | 11669 |
| Elektronik/Maskin (Electronics/Machinery) | 11634 |
| Fagforeninger/A-kasser/Pensionskasser (Unions/Unemployment funds/Pension funds) | 16791 |
| Finans/Forsikring/Pension (Finance/Insurance/Pension) | 10358 |
| Forskning/Uddannelse (Research/Education) | 10442 |
| Fødevarer/Dagligvarer (Food/Groceries) | 15407 |
| Handel/Service (Trade/Service) | 10364 |
| IT/Tele | 10331 |
| Klima/Energi/Forsyning (Climate/Energy/Utilities) | 17209 |
| Kommuner (Municipalities) | 16826 |
| Kultur/Medier/Underholdning (Culture/Media/Entertainment) | 10341 |
| Medicinal/Biotek/Kemi (Pharmaceuticals/Biotech/Chemistry) | 10333 |
| Papir/Møbel/Materialer (Paper/Furniture/Materials) | 10363 |
| Regioner/Sundhed/Socialvæsen (Regions/Health/Social services) | 11626 |
| Rådgivning/Konsulentservice (Consulting services) | 15586 |
| Stat/Politik/Samfund (Government/Politics/Society) | 10362 |
| Transport | 10440 |
| Vikar/Rekruttering (Temp/Recruitment) | 12450 |
### Suitability (andet)
| Label | ID |
|-------|-----|
| Nyuddannede (New graduates) | 2 |
| Personer med international baggrund (People with international background) | 4 |
| Erfarne (Experienced) | 5 |
### Remote Work (fjernarbejde)
| Label | Value |
|-------|-------|
| Helt hjemmearbejde (Fully remote) | `helt` |
| Delvist hjemmearbejde (Partially remote) | `delvist` |
## Job Detail URL Pattern
```
https://jobbank.dk/job/{id}/{company-slug}/{title-slug}/
```
Example: `https://jobbank.dk/job/12345/novo-nordisk/senior-data-scientist/`
## RSS Feed URL
Every search has an RSS equivalent:
```
https://jobbank.dk/job/rss?{same_query_params}
```
Example:
```
https://jobbank.dk/job/rss?key=python&amt=2&cvtype=3
```
## Pagination
- Pages are 1-indexed (first page is `page=1` or no page parameter)
- Approximately 20 results per page
- Use `&page=2`, `&page=3`, etc. to navigate
## Job Card Extraction
### CSS Selectors
| Element | Selector |
|---------|----------|
| Job card container | `div.job-item` |
| Job ID | `div.job-item[name]` (use `getAttribute("name")`) |
| Job title | `.job-header` |
| Job type/company/location | `.job-teaser` |
| Job description excerpt | `.job-description` |
| Date updated | `.job-date-updated` |
| Application deadline | `.job-date-application` |
| Job detail link | `a[href^="/job/"]` (prepend `https://jobbank.dk`) |
### Working Extraction Code
```javascript
async page => {
const jobs = await page.evaluate(() => {
return Array.from(document.querySelectorAll("div.job-item")).map(item => {
const id = item.getAttribute("name");
const link = item.querySelector("a[href^='/job/']");
const url = link ? "https://jobbank.dk" + link.getAttribute("href") : null;
const header = item.querySelector(".job-header");
const teaser = item.querySelector(".job-teaser");
const desc = item.querySelector(".job-description");
const dateUpdated = item.querySelector(".job-date-updated");
const deadline = item.querySelector(".job-date-application");
return {
id,
title: header ? header.textContent.trim() : null,
teaser: teaser ? teaser.textContent.trim() : null,
description: desc ? desc.textContent.trim() : null,
url,
dateUpdated: dateUpdated ? dateUpdated.textContent.trim() : null,
deadline: deadline ? deadline.textContent.trim() : null,
};
});
});
return JSON.stringify(jobs, null, 2);
}
```
## Examples
### Simple Keyword Search
```
https://jobbank.dk/job/?key=data+scientist
```
### Full-Time IT Jobs in Copenhagen
```
https://jobbank.dk/job/?key=developer&cvtype=3&udd=24&amt=2
```
### Remote Software Development Positions
```
https://jobbank.dk/job/?erf=31&fjernarbejde=helt
```
### Graduate/Trainee Positions in Multiple Cities
```
https://jobbank.dk/job/?cvtype=6&amt=2&amt=8
```
### Project Management Jobs Suitable for New Graduates
```
https://jobbank.dk/job/?key=project+manager&erf=27&andet=2
```
### Jobs in IT/Telecom Industry, Excluding Senior Positions
```
https://jobbank.dk/job/?key=developer&antikey=senior&branche=10331
```
### Data Analysis Jobs Posted in Last 7 Days
```
https://jobbank.dk/job/?key=data+analysis&erf=43&oprettet=2026-02-03
```
### Second Page of Python Jobs in Aarhus
```
https://jobbank.dk/job/?key=python&amt=8&page=2
```
### Specific Company Search (Novo Nordisk)
```
https://jobbank.dk/job/?virk=novo+nordisk
```
### Ph.D. and Postdoc Positions in Natural Sciences
```
https://jobbank.dk/job/?cvtype=12&udd=30
```
## Notes
- **URL encoding:** Use `+` or `%20` for spaces in text parameters (`key`, `antikey`, `virk`)
- **Multiple filters:** Repeat parameter names for multiple values in same category
- **Date format:** Use ISO format `YYYY-MM-DD` for `oprettet` parameter
- **Remote work:** Only two values: `helt` (fully) or `delvist` (partially)
- **Case sensitivity:** Filter IDs are case-sensitive, use exact values from tables
- **Empty results:** Invalid filter combinations may return zero results
- **RSS monitoring:** Use RSS feeds for automated job monitoring and alerts