---
name: linkedin-jobs
description: "Search LinkedIn job postings by keyword, location, date posted, work type, experience level and job type: title, company, location, full description, seniority, employment type and applicant count. No login, no browser. Use when the user wants current job postings from LinkedIn: jobs for a role, skill or company in a city or country, remote jobs, hiring research or job market scans. Filters by date posted, work type, experience level and job type. Calls the Pullwire API with the key in PULLWIRE_API_KEY; costs 1 credit per job posting."
metadata:
  openclaw:
    requires:
      env:
        - PULLWIRE_API_KEY
      bins:
        - curl
    primaryEnv: PULLWIRE_API_KEY
    homepage: https://pullwire.dev/tools/linkedin-jobs
---

# LinkedIn Jobs Scraper

Search LinkedIn job postings by keyword, location, date posted, work type, experience level and job type: title, company, location, full description, seniority, employment type and applicant count. No login, no browser. The data comes from the Pullwire API: you send the input as JSON and get structured JSON back. Only public data.

## Setup (once)

The API key must be in the environment variable `PULLWIRE_API_KEY`. Keys start with `af_live_`. Check it without printing it:

```bash
test -n "$PULLWIRE_API_KEY" && echo "key is set" || echo "PULLWIRE_API_KEY is missing"
```

If it is missing, stop and tell the user: sign up at https://pullwire.dev/login (new accounts get 2,000 free credits), create a key at https://pullwire.dev/app/keys, then run `export PULLWIRE_API_KEY="af_live_..."` in the shell the agent uses. Never print the key, write it into a file or repeat it in the chat.

## Run it

```bash
curl -sS -X POST "https://pullwire.dev/api/v1/tools/linkedin-jobs/runs" \
  -H "x-api-key: $PULLWIRE_API_KEY" \
  -H "content-type: application/json" \
  -d '{"searches":[{"keywords":"software engineer","location":"Germany"}],"maxResults":3}'
```

The request waits up to 25 seconds and answers with `{"run": {...}, "results": [...]}`. Check `run.status` (`succeeded` or `failed`) and `run.error`: when it is set, the run stopped early and `results` is partial (everything in it was charged). `run.creditsUsed` is what the run cost. For big answers add `-o result.json` and read only the fields you need.

Good to know:

- Every entry in `searches` needs `keywords` or a `location` (or both).
- Set `includeDescription` to `false` when titles, companies and locations are enough: it is faster.
- With `workType` set to `remote`, `location` still shows the city the employer listed and results have no separate remote field: the filter is applied by LinkedIn. Say so when you list jobs as remote.
- LinkedIn stops showing results after about 1,000 jobs per search, whatever you ask for.

## Input

The JSON body is the tool input. Fields:

| Field | Type | Required | Default | What it does |
| --- | --- | --- | --- | --- |
| `searches` | object[] | yes |  | Searches: One entry per keyword/location combination. |
| `searches[].keywords` | string | no |  | Keywords: Job title, skill or company, e.g. "software engineer". |
| `searches[].location` | string | no |  | Location: City, region or country, e.g. "Berlin, Germany" or "United States". |
| `searches[].datePosted` | "any" \| "past24h" \| "pastWeek" \| "pastMonth" | no | `"any"` | Date posted: Only jobs posted within this window. |
| `searches[].workType` | "any" \| "onsite" \| "remote" \| "hybrid" | no | `"any"` | Work type |
| `searches[].experienceLevel` | "any" \| "internship" \| "entry" \| "associate" \| "mid-senior" \| "director" \| "executive" | no | `"any"` | Experience level |
| `searches[].jobType` | "any" \| "full-time" \| "part-time" \| "contract" \| "temporary" \| "internship" \| "volunteer" | no | `"any"` | Job type |
| `searches[].distanceMiles` | integer | no |  | Distance (miles): Search radius around the location, in miles. |
| `searches[].maxResultsPerSearch` | integer | no | `100` | Max results for this search: LinkedIn’s guest pages stop returning results past ~1000 regardless of this value. |
| `includeDescription` | boolean | no | `true` | Include full description: Fetch each job’s detail page for the full description, seniority, employment type, industries and applicant count. One extra request per job. |
| `maxResults` | integer | no |  | Max results: Stop after this many jobs in total across all searches. Empty: no extra cap. |

Without `maxResults` a run returns at most 20 results; the maximum is 200 per run. Credits for `maxResults` results are reserved before the run starts and the unused part is given back, so ask for what you need.

## Output

Each item in `results` has these fields:

| Field | Type | Always there |
| --- | --- | --- |
| `jobId` | string | yes |
| `title` | string | yes |
| `companyName` | string | no |
| `companyUrl` | string | no |
| `location` | string | no |
| `jobUrl` | string | yes |
| `postedAt` | string | no |
| `postedAtText` | string | no |
| `applyBadge` | string | no |
| `description` | string | no |
| `seniorityLevel` | string | no |
| `employmentType` | string | no |
| `jobFunction` | string | no |
| `industries` | string | no |
| `applicantsCount` | integer | no |
| `searchKeywords` | string | no |
| `searchLocation` | string | no |
| `scrapedAt` | string | yes |

## Cost

1 credit per job posting. One job posting returned from a search. Searches with zero matches are not charged. Check the balance with:

```bash
curl -sS "https://pullwire.dev/api/v1/me" -H "x-api-key: $PULLWIRE_API_KEY"
```

The answer has `balance` in credits. Tell the user what a run cost when it was more than a few credits.

## Bigger jobs

Add `?async=1` to the run URL to start a background run (up to 50 seconds). It answers `202` with `run.id` right away. Poll every few seconds until `status` is `succeeded` or `failed`, then fetch the results:

```bash
curl -sS "https://pullwire.dev/api/v1/runs/$RUN_ID" -H "x-api-key: $PULLWIRE_API_KEY"
curl -sS "https://pullwire.dev/api/v1/runs/$RUN_ID/results" -H "x-api-key: $PULLWIRE_API_KEY"
```

Add `?format=csv` to the results URL when the user wants a spreadsheet.

## Errors

Errors answer `{"error": {"code": "...", "message": "..."}}`.

| Status | Code | What to do |
| --- | --- | --- |
| 400 | `INVALID_INPUT` | The message names the field. Fix the input and retry; nothing was charged. |
| 401 | `UNAUTHORIZED` | The key is missing, wrong or revoked. Ask the user to set a valid key in `PULLWIRE_API_KEY`. |
| 402 | `INSUFFICIENT_CREDITS` | The balance cannot cover the reservation. Lower `maxResults` or tell the user the balance is too low. |
| 404 | `NOT_FOUND` | Unknown tool or run ID. |
| 429 | `RATE_LIMITED` | Used up its burst of 120 requests and its 60 per minute. Wait, then retry. |
| 500 | `INTERNAL_ERROR` | Retry later. If the body has `runId`, fetch the partial results with `GET /runs/{runId}/results`. |
| 503 | `TOOL_UNAVAILABLE` | Our latest automatic checks against this site failed, so its runs are paused. Nothing was reserved or charged. Tell the user and try again later. |
| 503 | `SERVICE_UNAVAILABLE` | Retry later. |

Docs: https://pullwire.dev/docs · Tool page: https://pullwire.dev/tools/linkedin-jobs
