LinkedIn Search vs FreeHire Search: Key Differences for Job Discovery in the AI Job Search Toolkit
Both linkedin-search and freehire-search are country-agnostic portal skills that follow the same CLI contract, but they differ fundamentally in data source, query model, and output granularity.
The ai-job-search repository by MadsLorentzen provides two distinct job discovery tools under .agents/skills/: linkedin-search and freehire-search. While both expose identical search and detail commands, analyzing the source code reveals they serve different technical architectures and use-cases. This guide breaks down how each works, when to choose one over the other, and how they integrate into automated workflows.
Core Architecture: HTML Scraping vs JSON API
The most significant difference lies in how each skill retrieves job data.
LinkedIn Search: HTML Scraping Pattern
linkedin-search operates as a web scraper. It fetches LinkedIn's public jobs-guest HTML pages and parses them on the fly. In cli/src/helpers.ts, the parseJobCards function uses regex extraction to split the raw HTML into job card chunks and extract fields like title, company, and location.
This approach requires no API key but is inherently fragile—LinkedIn's page structure can change without notice. The skill implements exponential back-off for 429/5xx responses, as noted in its SKILL.md.
FreeHire Search: REST API Pattern
freehire-search consumes a structured JSON API from freehire.me. The backend aggregates approximately 50 ATS platforms into a normalized schema. The CLI builds query strings from facet flags and calls /api/v1/agent/jobs/search, forwarding the JSON response directly with minimal post-processing.
Unlike LinkedIn's guest pages, this API is explicitly public and designed for programmatic access. The skill also supports self-hosting via the FREEHIRE_API_URL environment variable, pointing to the MIT-licensed strelov1/freehire project.
Query Models: Free-Text Location vs Controlled Facets
How you specify search parameters differs substantially between the two tools.
LinkedIn: Simple Location Strings
linkedin-search accepts a free-text --location (or -l) flag that passes directly to LinkedIn's search. This makes it genuinely global—any market LinkedIn serves works with the same CLI.
bun run .agents/skills/linkedin-search/cli/src/cli.ts \
search -q "product manager" -l "Berlin, Germany" --remote remote --format table
FreeHire: Structured Facet Flags
freehire-search uses controlled vocabulary facets: --region, --country, --city, --remote, --seniority, and more. Location is not free-text but a filtered index matching freehire's dictionaries.
bun run .agents/skills/freehire-search/cli/src/cli.ts \
search -q "backend engineer" --seniority senior --country DE --jobage 14 --format table
This design trade-off favors precision for technical roles over geographic flexibility.
Output Granularity: Preview vs Full Description
The information returned by search commands varies dramatically.
| Aspect | linkedin-search |
freehire-search |
|---|---|---|
search output |
Title, company, location, ID only (preview) | Full description included for every hit |
detail purpose |
Required to fetch complete job posting | Re-fetch by slug; rarely needed after search |
| Payload size | Minimal | Larger, but complete |
For linkedin-search, you must make a second request:
bun run .agents/skills/linkedin-search/cli/src/cli.ts \
detail 4426311357 --format plain
For freehire-search, detail is mainly for re-retrieval by slug:
bun run .agents/skills/freehire-search/cli/src/cli.ts \
detail golang-zensar-2bxu6dxm --format plain
Scope and Target Use-Cases
LinkedIn: General-Purpose, Any Industry
linkedin-search covers all industries and seniority levels. Use it when you need:
- LinkedIn-specific fields (seniority level, employment type, direct apply links)
- Ad-hoc searches across arbitrary geographies
- Familiar job board coverage without domain restrictions
Warning: The CLI displays a prominent ⚠️ Personal use only notice. Automated scraping violates LinkedIn's Terms of Service.
FreeHire: Tech-Focused, Structured Workflows
freehire-search is optimized for software engineering, data science, and DevOps roles. Its facet dictionaries align with technical job classifications. While non-technical listings exist, coverage is less reliable.
Best suited for:
- Programmatic pipelines filtering by seniority, remote status, and tech stack
- Multi-market technical searches with consistent output structure
- Self-hosted deployments requiring data sovereignty
Shared Infrastructure: Zero-Dependency Skills
Both tools are built as isolated skills under .agents/skills/ and declare allowed-tools: Bash(bun run …) in their respective SKILL.md files. This means:
- Zero runtime dependencies beyond
bunitself - Automatic discovery by the higher-level
/scrapeworkflow (see.claude/skills/job-scraper/search-queries.md) - Identical CLI contract:
searchfor listings,detailfor single postings,--formatfor output control
The framework treats them as portable binaries runnable in any fork without installation steps, as documented in SETUP.md.
Self-Hosting and Portability
Only freehire-search offers true backend portability. The FREEHIRE_API_URL environment variable allows pointing to private instances of the strelov1/freehire aggregator. This makes it viable for organizations requiring:
- Internal job board aggregation
- Compliance with data residency requirements
- Custom ATS integrations beyond the public freehire.me instance
linkedin-search has no self-hosting equivalent—it always communicates with LinkedIn's live infrastructure.
Summary
- Data source:
linkedin-searchscrapes HTML;freehire-searchcalls a REST JSON API - Query style: LinkedIn uses free-text locations; FreeHire uses controlled facet flags
- Result completeness: LinkedIn requires
detailcalls for full descriptions; FreeHire returns them insearch - Domain focus: LinkedIn is general-purpose; FreeHire targets technical roles
- Self-hosting: Only FreeHire supports private backend instances via
FREEHIRE_API_URL - Terms of Service: Both are personal-use-only, but LinkedIn scraping carries higher enforcement risk
Frequently Asked Questions
Can I use both skills in the same automated workflow?
Yes. The /scrape workflow in ai-job-search auto-discovers all skills under .agents/skills/ that match the search/detail CLI contract. You can query both LinkedIn and freehire.me in sequence, though you should respect rate limits and TOS restrictions for each source.
Why does linkedin-search need a separate detail command?
LinkedIn's guest pages only show preview information in search results. The full job description requires a separate page fetch. In cli/src/helpers.ts, the HTML parsing logic extracts different fields for search versus detail commands. This two-step pattern keeps initial queries lightweight but adds latency for complete data.
Is freehire-search limited to specific countries?
No—both skills are country-agnostic. However, freehire-search uses ISO country codes and controlled region vocabularies rather than free-text locations. The underlying aggregator indexes ~50 ATS platforms globally, though coverage density varies by market.
Which skill is better for high-volume automated scraping?
Neither is ideal for unsupervised high-volume use. Both include rate-limit handling with exponential back-off, but LinkedIn actively blocks scrapers and freehire.me operates as a best-effort service without SLA. For production pipelines, consider self-hosting the strelov1/freehire backend to eliminate external dependency risks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →