All skills
madslorentzen avatar

/freehire-search

@9187f89

Use this skill to search live software / tech / data / engineering job listings across many countries and markets (and remote) via the freehire.me aggregator's public API, or to look up a specific posting. It aggregates roles from ~50 ATS platforms into one schema, so a single skill covers many markets — but its faceted filtering (skills, category, seniority) is tuned tech-first, so scope triggers to technical roles. Trigger phrases: find a tech job, software job search, developer jobs, engineering vacancies, data/ML jobs, DevOps roles, remote developer jobs, "are there any <tech role> jobs in <place>", look up this freehire job posting.

Use this Skill: https://skilld.dev/gh/madslorentzen/ai-job-search/freehire-search

This session only. Nothing lands on disk.

url-reference.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

freehire.me API reference

The endpoints, parameters, and response shapes this skill depends on. This is the file to update if the freehire API changes. Base URL defaults to https://freehire.me and is overridable via the FREEHIRE_API_URL env var.

Authentication

None for reads. GET /api/v1/jobs/* and /companies/* are public; only per-user tracking mutations (apply/save/me) require a bearer API key, and this skill does not use them.

Verified against the live API:

Endpoint Status
GET /api/v1/agent/jobs/search 200
GET /api/v1/jobs/search 200 (the web variant; not used by this skill)
GET /api/v1/jobs/facets 200
GET /api/v1/jobs/{slug} 200
GET /api/v1/auth/me 401 (auth required — not used here)

Envelope

Every response is { "data": ..., "meta": {...}, "error": "..." }. Lists put the array in data and pagination in meta ({ total, limit, offset }); a single item puts the object in data. Errors are { "error": "<message>" } with a 4xx/5xx status (e.g. 404 → { "error": "not found" }).

GET /api/v1/agent/jobs/search

The endpoint the skill's search command uses. Full-text + facet search over open jobs, returning data: [job, …] with meta.total = the estimated match count.

It runs the same query as the web-facing /api/v1/jobs/search — same q, same facets, same ranking, same pagination guard (offset + limit ≤ 10000) — and differs in one respect: asked to, it replaces the search index's truncated description preview with the posting's full description read from the database. That is what lets a search of N roles stay one request instead of N + 1.

Two extra parameters control it:

Param Maps to CLI flag Notes
include_description (always true) Without it the endpoint serves the index preview, same as the web search.
description_format --description-format markdown (the skill's default), text, or html. An unrecognized value is not an error — the API falls back to html, so the CLI validates the flag itself.

Hydration is best-effort per hit: a result whose row has vanished from the database (the index lagging a just-removed job) keeps the preview rather than being dropped, so description is a full text in practice but never guaranteed to be.

A 404 from this path means the instance predates the endpoint (a self-hosted freehire behind FREEHIRE_API_URL), not a missing job; the CLI reports it as an error naming the path rather than as an empty result set.

GET /api/v1/jobs/search

The web variant of the same search — identical query surface, but description is always the index's truncated preview. The skill does not call it; it is listed here because the shared parameters below are documented against both.

Query parameters used by the skill:

Param Maps to CLI flag Notes
q --query / -q Keyword full-text query.
limit --limit / -n Page size. Default 25 in the CLI.
offset (derived) offset = (page - 1) * limit.
semantic_ratio (fixed 0) Keyword search; the semantic index is opt-in.
posted_within_days --jobage Restrict to postings from the last N days.
regions --region Repeatable; OR within the facet. Values like global, eu, us, apac, latam, cis.
countries --country Repeatable; ISO-3166 alpha-2 (lowercased).
cities --city Repeatable; display-name city.
seniority --seniority Repeatable; junior, middle, senior, staff, …
category --category Repeatable; backend, frontend, fullstack, devops, ml_ai, …
skills --skill Repeatable; canonical skill names.
company_slug --company Single company.
work_mode --remote remote | hybrid | onsite.
any facet param --facet key=value Escape hatch for the long tail (e.g. salary_min, visa_sponsorship, employment_type, english_level).

Repeated params (?seniority=senior&seniority=staff) are ORed within a facet; different facets are ANDed (geography ORs into one location group). Deep paging is bounded server-side (offset + limit ≤ 10000).

Job object (the fields the skill reads)

{
  "public_slug": "golang-zensar-2bxu6dxm", // -> result.id, and detail's <slug>
  "source": "oracle",
  "external_id": "…",
  "url": "https://…",                       // the real posting URL (ATS host)
  "title": "GOLANG",
  "company": "Zensar",
  "company_slug": "zensar",
  "location": "India",                       // free-text ATS location
  "description": "- …",                      // agent search: full text in the requested
                                             // format; elsewhere HTML, stripped client-side
  "skills": ["go", "kubernetes", …],         // dictionary facet (top-level)
  "work_mode": "remote",                     // may be absent
  "regions": ["apac"],                       // dictionary/hybrid facet
  "countries": ["in"],
  "cities": [],
  "collections": [],
  "posted_at": "2026-07-06T00:00:00Z",       // -> result.date (nullable)
  "created_at": "2026-07-06T15:25:…Z",
  "enrichment": {                             // nested, typed; {} when unenriched
    "seniority": "senior",
    "category": "backend",
    "employment_type": "full_time",
    "salary_min": 90000, "salary_max": 120000, "salary_currency": "EUR"
  }
}

The internal numeric id is deliberately never exposed; public_slug is the stable identifier.

GET /api/v1/jobs/{slug}

A single job by its public_slug. Returns the same job object in data. A closed posting is still served here (with a non-null closed_at); a missing slug is 404 { "error": "not found" }. The skill's detail command maps a 404 to a NOT_FOUND error on stderr.

GET /api/v1/jobs/facets

The market's facet-value distributions under an optional filter — each facet's live values with counts. data.facets is { <facet>: { <value>: <count> } }. This skill does not call it programmatically, but it is the vocabulary source the SKILL.md points users to (?q=<role> scopes the counts). Example: GET /api/v1/jobs/facets?q=react.

Parsing notes

  • The response is JSON, so there is no HTML card parsing (unlike the scraping portals). The only markup handling left client-side is detail's: /jobs/{slug} serves HTML, which cleanHtml (cli/src/helpers.ts) strips into readable text. Search descriptions arrive already rendered by the API and are passed through verbatim — stripping them again would undo the Markdown structure.
  • Fetch uses a browser-ish User-Agent, Accept: application/json, and exponential backoff with jitter on 429/5xx (max 6 retries). A connection error (API unreachable) fails fast with a clear message — no retry, since it is not transient server load — which is the graceful-degradation contract: an outage degrades this source quickly instead of hanging the caller.

Source: SKILL.md on GitHub

1 warning2d3 checks · Risk SAFE
  • Gen Agent Trust Hub2d

    The skill is generally safe, but it processes job descriptions from an external API, which could potentially contain indirect prompt injections.

  • Socket2d

    No alerts

  • Snyk2d

    Risk: MEDIUM · 1 issue

Signed by skilld at 9187f89. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 2 days ago
What it can do
Runs commands
version
1.0.0
context
fork
enabled
true
All 1 allowed tools
Bash(bun run .agents/skills/freehire-search/cli/src/cli.ts *)

README badge

README badge for madslorentzen/ai-job-search/freehire-search