* fix(jobnet-search): filter search by the ESCO occupation that occupations prints SKILL.md and the CLI README told users to pass the conceptUriDa UUID from `occupations` to `search --occupation-area` / `--occupation-group`. Those flags take the numeric area and group codes (10000, 10060), so a UUID there returned a successful search with zero ads. The API filters one occupation through its `occupations` parameter, which returns the 830 nurse ads for the sygeplejerske UUID. Add `search --occupation`, sent as `occupations`, accepting the full conceptUriDa or its trailing UUID, and point the docs at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * changelog: place the #649 entry under [Unreleased] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com>
12 KiB
jobnet-cli
CLI for the Jobnet.dk Danish government job portal API.
Base URL: https://jobnet.dk/bff
Authentication: No credentials required. All public endpoints only need the x-csrf: 1 request header.
Format: All responses are JSON.
Installation
cd .agents/skills/jobnet-search/cli
bun install
Commands
| Command | Description |
|---|---|
search |
Search for job ads with filters |
detail |
Full detail for a single job ad |
occupations |
Search occupation types (for building filters) |
suggestions |
Typeahead suggestions for job title / keyword search |
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.
Regions
| Value | Danish region |
|---|---|
HovedstadenOgBornholm |
Hovedstaden og Bornholm |
Midtjylland |
Midtjylland |
Syddanmark |
Syddanmark |
OevrigeSjaelland |
Øvrige Sjælland |
Nordjylland |
Nordjylland |
Order types
| Value | Description |
|---|---|
PublicationDate |
Newest postings first (default) |
BestMatch |
Relevance score (requires --search-string) |
ApplicationDate |
Earliest deadline first |
search — Search for job ads
Endpoint: GET /FindJob/Search
bun run src/cli.ts search [flags]
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--search-string |
string | — | Free-text keyword search (job title, skills, employer) |
--region |
string | — | One region value (see Regions table) |
--postal-code |
string | — | Postal code for radius search, e.g. 2100 |
--radius |
number | 50 |
Radius in km from postal code (requires --postal-code) |
--work-hours |
string | — | FullTime or PartTime |
--duration |
string | — | Permanent or Temporary |
--job-type |
string | — | Announcement type: Ordinaert, Efterloenner, Foertidspension |
--occupation-area |
string | — | Occupation area identifier, e.g. 10000 |
--occupation-group |
string | — | Occupation group identifier, e.g. 10060 |
--occupation |
string | — | One ESCO occupation: the conceptUriDa from occupations, or its trailing UUID (sent as occupations) |
--page |
number | 1 |
Page number (1-indexed) |
--per-page |
number | 10 |
Results per page |
--limit |
number | — | Cap total results returned by CLI |
--order |
string | PublicationDate |
Sort order (see Order types table) |
--format |
string | json |
Output format: json, table, plain |
Example
bun run src/cli.ts search \
--search-string "sygeplejerske" \
--region HovedstadenOgBornholm \
--work-hours FullTime \
--duration Permanent \
--per-page 5 \
--format table
bun run src/cli.ts search \
--postal-code 8000 \
--radius 25 \
--per-page 10
Response shape
{
"meta": {
"totalJobAdCount": 21452,
"pageNumber": 1,
"resultsPerPage": 10,
"searchString": "developer"
},
"facets": {
"regions": [
{ "type": "HovedstadenOgBornholm", "jobAdCount": 6642 }
],
"workHours": [
{ "type": "FullTime", "jobAdCount": 17919 },
{ "type": "PartTime", "jobAdCount": 3533 }
],
"employmentDurations": [
{ "type": "Permanent", "jobAdCount": 18533 },
{ "type": "Temporary", "jobAdCount": 2919 }
],
"occupationAreas": [
{ "identifier": "10000", "jobAdCount": 3235 }
],
"countries": [
{ "label": "Danmark", "identifier": "DK", "jobAdCount": 21164 }
]
},
"results": [
{
"jobAdId": "9ef43bce-d82b-4ea1-a098-7ff6520f99be",
"title": "Akademisk medarbejder med interesse for arbejdsmiljø og uddannelse",
"hiringOrgName": "Region Midtjylland",
"occupation": "Personalekonsulent",
"municipality": "Viborg",
"postalCode": 8800,
"postalDistrictName": "Viborg",
"country": "Danmark",
"publicationDate": "2026-03-13T00:00:00+01:00",
"applicationDeadline": "2026-04-05T21:59:00+02:00",
"applicationDeadlineStatus": "ExpirationDate",
"workHourPartTime": false,
"isExternal": false,
"jobAdUrl": null,
"hasLogo": true,
"logoUrl": "/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be",
"cvr": "29190925",
"workPlaceAddress": "",
"conceptUriDa": "http://data.star.dk/esco/occupation/426e017f-ebe5-4bea-b1eb-7d2d5ab3c6db",
"isSeen": false,
"isFavorite": false,
"company": "Region Midtjylland",
"location": "Viborg",
"date": "2026-03-13",
"deadline": "2026-04-05",
"url": "https://jobnet.dk/find-job/9ef43bce-d82b-4ea1-a098-7ff6520f99be"
}
]
}
Note
: The
descriptionfield (raw HTML) is intentionally omitted fromsearchresults for brevity. Usedetailto retrieve the full job description.
Note
: Every result also carries the cross-portal contract fields
company,location,date,deadlineandurl- derived respectively fromhiringOrgName,postalDistrictName/municipality, and the destination URL (the employer'sjobAdUrlfor external ads, or/find-job/{jobAdId}for internal ones)./scrapeStep 2 expects search output to include title, company, location, date, and URL, and dates follow theYYYY-MM-DDconvention of the other portal CLIs. The API's1900-01-01deadline sentinel (deadline not disclosed) maps tonull. The native fields above are preserved unchanged.
Note
:
resultsPerPageandpageNumbermust always be provided — omitting them while also providingsearchStringcauses the API to return error 1014 ("Fejl i formatering af inputs").
detail — Full job ad detail
Endpoint: GET /FindJob/JobAdDetails/{id}
bun run src/cli.ts detail <id> [--format json|plain]
The id is the jobAdId UUID from search results.
By default the CLI passes incrementViews=false to avoid polluting view counts.
Example
bun run src/cli.ts detail 9ef43bce-d82b-4ea1-a098-7ff6520f99be
bun run src/cli.ts detail 9ef43bce-d82b-4ea1-a098-7ff6520f99be --format plain
Response shape
{
"id": "9ef43bce-d82b-4ea1-a098-7ff6520f99be",
"title": "Akademisk medarbejder med interesse for arbejdsmiljø og uddannelse",
"body": "<p>Full HTML job description...</p>",
"publicationDateTime": "2026-03-13T08:06:58.0578635+01:00",
"unpublicationDateTime": "2026-04-05T21:59:00+02:00",
"approvalStatus": "Godkendt",
"views": 2,
"createdDateTime": "2026-03-13T08:06:58.3936145+01:00",
"updatedDateTime": "2026-03-13T08:06:58.3936145+01:00",
"isAnonymousEmployer": false,
"hasLogo": true,
"logoUrl": "/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be",
"employer": {
"cvrNumber": "29190925",
"pNumber": "1003367314",
"name": "Region Midtjylland",
"hasCompanyLogo": true
},
"job": {
"type": "Ordinaert",
"address": {
"streetName": "Heibergs Alle 5A",
"city": "Viborg",
"postalCode": "8800",
"municipality": "Viborg",
"countryCode": "DK",
"countryName": "Danmark"
},
"noFixedWorkplace": false,
"isLimitedPeriod": false,
"isDisabilityFriendly": false,
"isPartTime": false,
"employmentDate": "2026-06-01T00:00:00+02:00",
"conceptUriDa": "http://data.star.dk/esco/occupation/426e017f-ebe5-4bea-b1eb-7d2d5ab3c6db",
"preferredLabelDa": "Personalekonsulent",
"driversLicenses": [],
"classifications": [],
"shifts": [],
"isFavorite": false
},
"application": {
"deadlineDate": "2026-04-05T21:59:00+02:00",
"availablePositions": 1,
"contactPersons": [
{
"firstNames": "Jane",
"lastName": "Doe",
"phoneNumber": "+4512345678"
}
],
"url": "https://midtjob.dk/ad/...",
"urlText": "",
"isApplicationDeadlineASAP": false
},
"organisationTypeId": 24,
"user": "Emply 31430747"
}
Note
:
bodycontains raw HTML. In--format plainoutput the CLI strips HTML tags to produce readable text.
Note
:
application.urlmay be empty for some job ads — the employer may only accept applications through Jobnet's internal system.
occupations — Search occupation types
Endpoint: GET /OccupationSearch
bun run src/cli.ts occupations --search-string <text> [flags]
Use this command to find occupation identifiers (ESCO concept URIs) to pass to search --occupation.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--search-string |
string | required | Search term for occupation, e.g. sygeplejerske |
--per-page |
number | 10 |
Max results to return |
--format |
string | json |
Output format: json, table, plain |
Example
bun run src/cli.ts occupations --search-string "sygeplejerske" --per-page 5
Response shape
[
{
"conceptUriDa": "http://data.star.dk/esco/occupation/56f5d45c-1234-4321-abcd-000000000000",
"preferredLabelDa": "Sygeplejerske",
"aliases": [
{
"aliasIdentifier": "some-uuid",
"conceptUriDa": "http://data.star.dk/esco/occupation/56f5d45c-1234-4321-abcd-000000000000",
"alternativeLabelDa": "Operationssygeplejerske"
}
]
}
]
Note
:
conceptUriDais the full ESCO concept URI. Pass it, or its UUID (last path segment), tosearch --occupation. The numeric--occupation-area/--occupation-groupcodes are a different taxonomy and match no ads for a UUID.
suggestions — Typeahead suggestions
Endpoint: GET /FindJob/GetTypeaheadSuggestions
bun run src/cli.ts suggestions --query <text> [flags]
Returns autocomplete strings for the search box — useful for exploring valid Danish job titles before running a search.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--query |
string | required | Partial search string to complete |
--limit |
number | — | Cap number of suggestions returned |
--format |
string | json |
Output format: json, table, plain |
Example
bun run src/cli.ts suggestions --query "syge"
bun run src/cli.ts suggestions --query "ingeniør" --limit 5
Response shape
[
"sygepleje",
"Sygeplejerske",
"Sygeplejerske \"Lægeassistent\""
]
Note
: Suggestions are tuned for Danish job titles. English terms like "developer" may return an empty array.
Error handling
All errors are written to stderr in JSON format and exit with code 1:
{ "error": "Job ad not found", "code": "NOT_FOUND" }
{ "error": "API request failed: 500 Internal Server Error", "code": "API_ERROR" }
{ "error": "--query is required", "code": "MISSING_REQUIRED" }
{ "error": "--search-string is required", "code": "MISSING_REQUIRED" }
URL construction
Job ad detail pages on jobnet.dk:
https://jobnet.dk/find-job/{jobAdId}
The legacy https://jobnet.dk/job/{jobAdId} route redirects anonymous visitors into the
MitID login flow, so it is never emitted. External ads (isExternal: true) 404 on /find-job/
and hit the login wall on /job/ - the search command routes external ads to their destination
in jobAdUrl (falling back to /find-job/{jobAdId} when missing or not http(s)) instead of
emitting a broken link.
Company logo images (prefix relative logoUrl from API):
https://jobnet.dk{logoUrl}
Example: https://jobnet.dk/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be
Notes
- All data is from the public
jobnet.dk/bffREST API — no credentials required. - The
x-csrf: 1header must be sent with every request. - Pagination is 1-indexed (
--page 1is the first page). searchresults intentionally omit the HTMLdescriptionfield — usedetailto fetch it.bodyindetailresponses is raw HTML; use--format plainto get stripped text.- The
occupationscommand helps discover ESCO occupation URIs to pass tosearch --occupationfor narrowing search results.