* fix(jobnet-search): emit source: search-fallback on stdout for external ads (#549) Move the degraded search summary signal from stderr to stdout as a source: search-fallback field on DetailApiResponse. Keeps stderr quiet on success across all portal CLIs, renders the degraded signal in plain and table format outputs, documents the optional field in the CLI README, and covers the fallback execution path with tests. * changelog: place the #550 entry under [Unreleased] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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} (falls back to GET /FindJob/Search?searchString={id} on 404 for external ads)
bun run src/cli.ts detail <id> [--format json|table|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
: The optional
sourcefield indicates whether detail was synthesized from the search endpoint fallback ("search-fallback") whenJobAdDetailsreturned 404 for an external ad. Direct responses omitsource. When present, plain and table formats render aSource: search-fallback (detail unavailable, synthesised from search)line.
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.