Files
ai-job-search/.agents/skills/jobnet-search/cli
Mads LorentzenandClaude Fable 5 f3d4448cca fix: restore Danish CLI commands/ and tsconfig files dropped by old .gitignore (#53)
The pre-#21 .gitignore's unanchored 'commands/' rule silently excluded
.agents/skills/*/cli/src/commands/ (and the tsconfigs) from the initial
release, so every clone's four Danish portal CLIs failed on import with
'Cannot find module ./commands/search.js'. #21 fixed the rule but the
files were never restored - git history has no trace of them.

Restored from the maintainer's working copies, including the updated
jobindex helpers.ts (Jobindex moved search results from the JSON
endpoint, which now returns 204, into an embedded HTML Stash blob).

Verified: all four CLIs typecheck and return live results with their
documented flags. Surfaced while reviewing #52.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 17:39:47 +02:00
..

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 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
--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,
      "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
    }
  ]
}

Note

: The description field (raw HTML) is intentionally omitted from search results for brevity. Use detail to retrieve the full job description.

Note

: resultsPerPage and pageNumber must always be provided — omitting them while also providing searchString causes 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

: body contains raw HTML. In --format plain output the CLI strips HTML tags to produce readable text.

Note

: application.url may 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 as filters to search.

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

: conceptUriDa is the full ESCO concept URI. The UUID portion (last path segment) can be used to build occupation filters for search.


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/job/{jobAdId}

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/bff REST API — no credentials required.
  • The x-csrf: 1 header must be sent with every request.
  • Pagination is 1-indexed (--page 1 is the first page).
  • search results intentionally omit the HTML description field — use detail to fetch it.
  • body in detail responses is raw HTML; use --format plain to get stripped text.
  • The occupations command helps discover ESCO occupation URIs usable as --occupation-area / --occupation-group seeds for narrowing search results.