# Google Jobs Scraper & Monitor (`suslabs/google-jobs-scraper`) Actor

Scrape Google Jobs: full descriptions, every apply link, parsed salary and remote/hybrid type. Monitor mode returns only new and changed jobs for daily alerts to Slack, webhooks or n8n. Covers LinkedIn, Indeed, Glassdoor and more. Pay per result.

- **URL**: https://apify.com/suslabs/google-jobs-scraper.md
- **Developed by:** [SusLabs](https://apify.com/suslabs) (community)
- **Categories:** Jobs, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Jobs Scraper & Monitor

Turn any Google Jobs search into clean job data, or into a daily feed of **only new and changed jobs**. Google Jobs collects postings from LinkedIn, Indeed, Glassdoor, ZipRecruiter, company career sites and hundreds of other boards, so one search covers many sources.

- 📄 **Full job descriptions**, not snippets: the complete text Google shows, with highlights where available.
- 🔗 **Every apply link** Google lists (often 5–8 boards per job), plus one recommended `primaryApplyUrl`.
- 💰 **Salary parsed** into min / max / currency / period ($, £, ₹, K, lakh/LPA), with the raw text kept.
- 🆕 **Monitor mode**: a named monitor returns only NEW jobs and meaningful CHANGES (salary, title, location, description, main apply link). Jobs you already have are never charged again.
- 🕒 **Freshness filters** (past day, 3 days, week, month) push Google towards recent postings.
- 🚦 **Honest run summary**: every search reports OK, EMPTY, BLOCKED or FAILED, so "nothing new" never looks like "something broke".
- 🔔 **Slack, Discord or signed webhook alerts**, plus a ready n8n workflow.
- 💸 **Pay per result**: no monthly fee; failed searches and duplicates are free.

### What can I use it for?

| I want to… | Use |
| --- | --- |
| Get a daily list of new roles for my recruiting niche | Monitor mode + `datePosted: "3 days"` on a daily schedule |
| Feed fresh jobs into my job board or database | Search or monitor mode; upsert by `jobId` |
| Compare salaries posted for a role across cities | Explicit searches, one per city, then the **Salary** view |
| See which companies are hiring for a skill | Search mode; group by `company` |
| Pipe new jobs into Slack, Sheets or a CRM | Monitor mode + alerts or the n8n workflow |

### Quick start

**One search** (about 10 jobs):

```json
{ "queries": ["data analyst"], "location": "Austin, TX", "country": "us" }
```

**A daily monitor** that returns only new and changed jobs:

```json
{
  "mode": "monitor",
  "monitorName": "austin-analysts",
  "queries": ["data analyst"],
  "location": "Austin, TX",
  "country": "us",
  "datePosted": "3 days",
  "firstRun": "baseline"
}
```

Run it on an Apify [schedule](https://docs.apify.com/platform/schedules) once a day. `firstRun: "baseline"` records today's jobs without charging for them, so you only pay for jobs that appear afterwards. Use `"emit"` to receive the first batch too.

**Many searches at once** (no hidden query × city multiplication):

```json
{
  "searches": [
    { "id": "nyc-nurse", "query": "registered nurse", "location": "New York, NY", "country": "us" },
    { "id": "london-nurse", "query": "registered nurse", "location": "London", "country": "uk" },
    { "id": "blr-python", "query": "python developer", "location": "Bengaluru", "country": "in" }
  ]
}
```

### How many jobs per search?

Google Jobs shows **about 10 jobs per search**, and that first page is what this Actor reads. It doesn't page deeper. To cover more ground, add searches (more cities, more job titles), or turn on **extra searches**:

```json
"extraSearches": { "enabled": true, "freshness": ["any", "week", "month"], "nearbyCities": ["Round Rock, TX"], "queryVariants": ["analytics engineer"] }
```

Each extra search is one more first page, billed as one search page. Different date windows often return different jobs. In our tests, four windows gave about 25 unique jobs instead of 10. Jobs that appear in several searches are delivered and charged **once**, with every matching search listed in `searchIds`.

### Output

Each dataset row is one job event. Main fields:

| Field | Example / meaning |
| --- | --- |
| `eventType` | `SNAPSHOT` (search mode), `NEW` or `CHANGED` (monitor mode) |
| `jobId` | Google's job ID. Use it to upsert. |
| `title`, `company`, `jobLocation`, `publisher` | "Data Analyst IV", "Texas HHSC", "Austin, TX", "Indeed" |
| `description`, `descriptionStatus` | Full text; `FULL` / `PREVIEW` / `MISSING` |
| `highlights` | Qualifications, responsibilities and benefits, when Google shows them |
| `primaryApplyUrl`, `applyOptions[]` | Recommended link, plus every board Google lists (tracking parameters removed) |
| `salary` | `{raw, min, max, currency, period}` |
| `postedAgeText`, `postedAtEstimate` | "3 days ago", estimated timestamp |
| `employmentType`, `workplaceType` | Full-time / Contractor…; remote / hybrid / onsite / unknown, with evidence |
| `firstSeenAt`, `lastSeenAt` | When this monitor first and last saw the job |
| `changes[]` | For CHANGED rows: `{field, before, after}`. Changes to the extra boards Google rotates in and out are ignored. |

Output views in the Console: **Overview**, **Salary**, **Apply links** and **Changes**. Download as JSON, CSV, Excel or via the API.

The run's key-value store has a **SUMMARY** record: each search's outcome, jobs found, new, changed, filtered and charges.

### Pricing

Pay-per-event: you pay only for what you get.

| Event | Free | Starter | Scale | Business |
| --- | ---: | ---: | ---: | ---: |
| Run start | $0.003 | $0.003 | $0.003 | $0.003 |
| Search page checked | $0.008 | $0.007 | $0.006 | $0.005 |
| Job delivered | $4.00 / 1,000 | $3.50 / 1,000 | $3.00 / 1,000 | $2.50 / 1,000 |
| Job change delivered | $1.00 / 1,000 | $1.00 / 1,000 | $1.00 / 1,000 | $1.00 / 1,000 |

**No charge for:** failed, blocked or retried searches (only the run start applies), duplicate jobs, filtered-out jobs, and unchanged jobs in a monitor.

Examples on the Free plan:

- One search with 10 jobs: about **$0.05**.
- A 50-search batch with ~500 jobs: about **$2.40**.
- A daily one-search monitor with nothing new: about **$0.33 per month**, plus $0.004 for each new job.

Set **Maximum cost per run** in the run options to cap any run.

### Filters

| Filter | How it works |
| --- | --- |
| `datePosted` | Asks Google for recent jobs, then drops any whose age is outside the window. Jobs with no age shown are kept and marked unknown. "past day" can return very few jobs; "3 days" works best for daily monitors. |
| `employmentType`, `remoteOnly`, `titleInclude`, `titleExclude`, `companyExclude`, `requireSalary` | Applied to the jobs found. Filtered jobs are free. |

### Use it from code, n8n or AI agents

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("suslabs/google-jobs-scraper").call(
    run_input={"queries": ["data analyst"], "location": "London", "country": "uk"},
    max_total_charge_usd=0.10,
)
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["title"], "|", job["company"], "|", job["primaryApplyUrl"])
```

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('suslabs/google-jobs-scraper').call(
  { queries: ['data analyst'], location: 'London', country: 'uk' },
  { maxTotalChargeUsd: 0.10 },
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```bash
curl -X POST "https://api.apify.com/v2/acts/suslabs~google-jobs-scraper/run-sync-get-dataset-items?maxTotalChargeUsd=0.10" \
  -H "Authorization: Bearer <YOUR_APIFY_TOKEN>" -H "Content-Type: application/json" \
  -d '{"queries":["data analyst"],"location":"London","country":"uk"}'
```

- **n8n:** start the Actor on a schedule with the Apify node (or an HTTP request), wait for the run, read the dataset and send NEW jobs to Slack, Sheets or your database. Upsert by `jobId`; deduplicate notifications by `eventId`.
- **Alerts without n8n:** set `alerts` to a Slack, Discord or webhook URL. Webhook deliveries can be signed with HMAC-SHA256 (`alertSecret`); deduplicate by `deliveryId`.
- **AI agents (MCP):** `https://mcp.apify.com?tools=suslabs/google-jobs-scraper`

### Limits, stated plainly

- About 10 jobs per search (Google's first page). Not every job in a city.
- Tested in the **US, UK and India**. Other countries usually work, but results are marked unverified.
- Descriptions are the text Google shows. The employer's site isn't visited.
- Google sometimes groups listings that don't match under one job. All apply links are kept as Google shows them; `primaryApplyUrl` is our best pick.
- A job disappearing from Google doesn't mean it closed, so this Actor never marks jobs as closed.
- Google can change its pages at any time. We run a daily health check and fix breakages quickly.

### FAQ

**Why did my monitor return nothing?** Check SUMMARY. A search marked OK with no new jobs is healthy: nothing new appeared since the last run.

**Can I change a monitor's searches?** Use a new `monitorName` for a different set of searches or filters. A monitor rejects changed inputs so it never mixes two scopes.

**How long does a monitor remember jobs?** 90 days after a job was last seen.

**Is it legal?** It reads public, logged-out Google Jobs pages only: no login, no CAPTCHA solving. You're responsible for how you use the data, including privacy and employment laws where you operate.

**Found a problem?** Open an issue on the **Issues** tab with the run ID and your input (without secrets).

# Actor input Schema

## `queries` (type: `array`):

Job titles or keywords to search, e.g. "data analyst". Each query is one Google Jobs search (about 10 jobs). Leave empty if you use Explicit searches.

## `location` (type: `string`):

City or region added to each query, e.g. "London" or "Bengaluru". Leave empty for no location.

## `country` (type: `string`):

US, UK and India verified. Other two-letter markets are unverified.

## `maxJobs` (type: `integer`):

Maximum delivered jobs. See the README for matching and billing limits.

## `mode` (type: `string`):

Mode. See the README for matching and billing limits.

## `searches` (type: `array`):

Use instead of queries. Objects: id, query, location, country, language. No hidden cross-product.

## `language` (type: `string`):

Language. See the README for matching and billing limits.

## `datePosted` (type: `string`):

Google freshness preference plus local age check; outliers dropped, unknown ages retained. Past day may use a 3-day Google window plus local24-hour check. Not exact or exhaustive.

## `employmentType` (type: `array`):

Employment types. See the README for matching and billing limits.

## `remoteOnly` (type: `boolean`):

Remote only. See the README for matching and billing limits.

## `requireSalary` (type: `boolean`):

Require parsed salary. See the README for matching and billing limits.

## `monitorName` (type: `string`):

Monitor name. See the README for matching and billing limits.

## `firstRun` (type: `string`):

First monitor run. See the README for matching and billing limits.

## `emitChanges` (type: `boolean`):

Emit material changes. See the README for matching and billing limits.

## `extraSearches` (type: `object`):

Opt-in separate billed first pages, never pagination: nearbyCities, queryVariants, or freshness windows \[any,3 days,week,month] applied to the first base search.

## `maxSearches` (type: `integer`):

Maximum searches. See the README for matching and billing limits.

## `maxConcurrency` (type: `integer`):

At most five simultaneous source requests; request deadline120 seconds, with one deferred retry only for timeouts.

## `alerts` (type: `object`):

Optional slack, discord, webhook HTTPS URLs. Kept out of results and logs.

## `alertSecret` (type: `string`):

Webhook signing secret. See the README for matching and billing limits.

## `webhookAuthorization` (type: `string`):

Generic webhook Authorization. See the README for matching and billing limits.

## `replayAlerts` (type: `object`):

Monitor only. deliveryIds\[] and/or since ISO timestamp; no Google fetch.

## `canary` (type: `boolean`):

Owner only. Ignore this field.

## `titleInclude` (type: `array`):

titleInclude. See the README for matching and billing limits.

## `titleExclude` (type: `array`):

titleExclude. See the README for matching and billing limits.

## `companyExclude` (type: `array`):

companyExclude. See the README for matching and billing limits.

## Actor input object example

```json
{
  "queries": [
    "data analyst"
  ],
  "location": "Austin, TX",
  "country": "us",
  "maxJobs": 100,
  "mode": "search",
  "language": "en",
  "datePosted": "any",
  "employmentType": [],
  "remoteOnly": false,
  "requireSalary": false,
  "monitorName": "",
  "firstRun": "emit",
  "emitChanges": true,
  "extraSearches": {
    "enabled": false
  },
  "maxSearches": 20,
  "maxConcurrency": 5,
  "canary": false,
  "titleInclude": [],
  "titleExclude": [],
  "companyExclude": []
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `salary` (type: `string`):

No description

## `apply-links` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `routeHealth` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "data analyst"
    ],
    "location": "Austin, TX"
};

// Run the Actor and wait for it to finish
const run = await client.actor("suslabs/google-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": ["data analyst"],
    "location": "Austin, TX",
}

# Run the Actor and wait for it to finish
run = client.actor("suslabs/google-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "data analyst"
  ],
  "location": "Austin, TX"
}' |
apify call suslabs/google-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,suslabs/google-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ULV4FdnwdgbGLkAls/builds/bKsaeKZRPc9AMa28b/openapi.json
