# Gulf Jobs & Hiring Signals Monitor (`pharaoh_data/gulf-jobs-monitor`) Actor

Track jobs from Greenhouse, Lever, Ashby, and Workable career boards across Saudi Arabia, the UAE, and the GCC. Get new, updated, and closed jobs between runs, plus a per-company hiring summary.

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

## Pricing

from $1.50 / 1,000 job results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Gulf Jobs & Hiring Signals Monitor

### Know when your target companies start hiring in Saudi Arabia, the UAE, and the GCC

Point this Actor at the public career boards of the employers you care about. Each run returns clean, normalized job records from **Greenhouse, Lever, Ashby, and Workable**. With change tracking on, it also tells you which jobs are **new, updated, or closed** since the last run, plus a per-company hiring summary.

Schedule it daily and you get a hiring-signals feed you can send to a CRM, a Slack channel, a spreadsheet, or an AI agent.

**Built for:**

- **B2B sales and partnerships teams** who treat "company X just opened 5 roles in Riyadh" as a buying signal.
- **Recruiters and staffing agencies** who track which Gulf employers are hiring and for which teams.
- **Investors and market analysts** who follow hiring velocity at portfolio or competitor companies.
- **Job-board and HR-tech builders** who need a reliable, deduplicated feed straight from employer boards.

> The Actor reads only the employer boards you list. It does not search every job site and does not claim full market coverage.

### What you get

- **Four job-board systems, one schema:** Greenhouse (US and EU), Lever (US and EU), Ashby, and Workable. The provider is detected from the board URL.
- **GCC-aware locations:** each listing gets a `country` and a `countries` list for SA, AE, QA, KW, BH, and OM. Detection uses city and country names in English and Arabic (Riyadh/الرياض, Dubai/دبي, Doha/الدوحة, and more). Amman, Jordan is not confused with Oman.
- **Change tracking between runs:** every record is labelled `new`, `updated` (with `changedFields`), `unchanged`, or `closed`, with `firstSeenAt`, `lastSeenAt`, and `closedAt`.
- **"Changes only" mode:** scheduled runs return just what changed, so alerts stay short and runs stay cheap.
- **Hiring summary:** each company's matching, new, updated, and closed counts, broken down by country and top departments, in the `HIRING_SUMMARY` record.
- **Salary data where the employer publishes it:** salary text plus a structured `salaryRange` (min, max, currency, interval) from Lever and Ashby. Pay that an employer chose to hide is never output.
- **Arabic-aware filters:** keywords, titles, and locations match regardless of diacritics and spelling variants (أ/إ/ا, ى/ي, ة/ه).
- **Privacy by default:** email addresses, phone numbers, and personal LinkedIn links inside job descriptions are redacted unless you turn this off.
- **Clean, deduplicated output:** one record per posting, matched by canonical URL, with links to the original listing and application page.

### Quick start

1. Under **Employer job boards**, add the public board URLs, for example:
   - `https://jobs.lever.co/company`
   - `https://job-boards.greenhouse.io/company` (or `job-boards.eu.greenhouse.io`)
   - `https://jobs.ashbyhq.com/company`
   - `https://apply.workable.com/company/`
2. Optionally set a `companyName` label and a fallback `country` for each board. The fallback applies to listings that say only "Remote".
3. Choose countries, titles, locations, or keywords to narrow the results.
4. Run it. For monitoring, save the configuration as a **task**, add a **schedule** (for example daily at 07:00 Riyadh time), and switch **Output** to **Changes only**.

#### Example input

```json
{
    "sources": [
        { "boardUrl": "https://apply.workable.com/foodics/", "companyName": "Foodics", "country": "SA" },
        { "boardUrl": "https://jobs.ashbyhq.com/ziina", "companyName": "Ziina", "country": "AE" },
        { "boardUrl": "https://job-boards.eu.greenhouse.io/tamara", "companyName": "Tamara", "country": "SA" },
        { "boardUrl": "https://jobs.lever.co/kpler", "companyName": "Kpler", "country": "AE" }
    ],
    "countries": ["SA", "AE", "QA", "KW", "BH", "OM"],
    "jobTitles": ["engineer", "manager"],
    "keywords": [],
    "locations": [],
    "postedWithinDays": 0,
    "maxResults": 1000,
    "trackChanges": true,
    "outputMode": "changes",
    "includeClosed": true,
    "monitorId": "gulf-fintech-hiring",
    "includeDescriptions": true,
    "redactContactDetails": true
}
```

#### Input reference

| Field                  | Default     | What it does                                                                                                                  |
| ---------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `sources`              | required    | 1–100 public board root URLs. Optional per board: `provider`, `companyName`, `country` (fallback for location-less listings). |
| `countries`            | all six GCC | Keep listings located in these countries. Empty keeps every location worldwide.                                               |
| `jobTitles`            | `[]`        | Phrases matched in the title; any can match.                                                                                  |
| `locations`            | `[]`        | Terms matched in the location; any can match.                                                                                 |
| `keywords`             | `[]`        | Terms matched in title, description, department, company, or location; any can match.                                         |
| `postedWithinDays`     | `0`         | Keep listings published within N days. `0` disables it; listings without a date are kept.                                     |
| `maxResults`           | `1000`      | Maximum records saved per run, including closed-job records. Each record is one billable result.                              |
| `trackChanges`         | `true`      | Compare with the previous run of the same monitor.                                                                            |
| `outputMode`           | `all`       | `all` saves every matching job with its status; `changes` saves only new, updated, and closed jobs.                           |
| `includeClosed`        | `true`      | Save `closed` records for jobs an earlier run saved that their board no longer lists.                                         |
| `monitorId`            | auto        | Name for the change history. When empty it is derived from the boards and filters, so changing them starts a fresh history.   |
| `includeDescriptions`  | `true`      | Include plain-text descriptions (up to 20,000 characters).                                                                    |
| `redactContactDetails` | `true`      | Remove emails, phone numbers, and personal LinkedIn links from descriptions.                                                  |

Filter groups combine (country AND title AND location AND keyword AND date); values within a group are "any match".

### Output

Each dataset item is one job:

```json
{
    "jobId": "workable:foodics:9BDF0B39E6",
    "provider": "workable",
    "sourceCompany": "Foodics",
    "boardUrl": "https://apply.workable.com/foodics/",
    "country": "SA",
    "countries": ["SA"],
    "title": "AI Engineer",
    "description": "Who Are We? We Are Foodics! a leading restaurant management ecosystem…",
    "location": "Riyadh, Saudi Arabia",
    "workplaceType": "",
    "department": "Technology",
    "employmentType": "Full-time",
    "salary": "",
    "salaryRange": null,
    "postedAt": "2026-09-29T00:00:00.000Z",
    "jobUrl": "https://apply.workable.com/j/9BDF0B39E6",
    "applyUrl": "https://apply.workable.com/j/9BDF0B39E6/apply",
    "scrapedAt": "2026-10-17T16:20:14.687Z",
    "changeType": "new",
    "changedFields": [],
    "firstSeenAt": "2026-10-17T16:20:14.687Z",
    "lastSeenAt": "2026-10-17T16:20:14.687Z",
    "closedAt": null
}
```

- `workplaceType` is `onsite`, `remote`, `hybrid`, or empty when the source does not say.
- `changeType` is `null` when change tracking is off. Closed records keep the last known title, location, and links, and have an empty description.
- The run's **`HIRING_SUMMARY`** record (default key-value store) looks like this:

```json
{
    "generatedAt": "2026-10-17T16:21:09.824Z",
    "monitorId": "gulf-fintech-hiring",
    "outputMode": "changes",
    "storedRecords": 2,
    "totals": {
        "sources": 4,
        "failedSources": 0,
        "matchingJobs": 66,
        "new": 0,
        "updated": 1,
        "unchanged": 65,
        "closed": 1
    },
    "companies": [
        {
            "company": "Ziina",
            "provider": "ashby",
            "status": "ok",
            "listingsFetched": 13,
            "matchingJobs": 13,
            "new": 0,
            "updated": 1,
            "closed": 0,
            "byCountry": { "AE": 13 },
            "topDepartments": [{ "name": "Engineering / Internal Platform", "count": 4 }]
        }
    ]
}
```

### Use it from your code, n8n, Make, or an AI agent

Run and fetch results in one call (replace the token; keep it out of shared code):

```bash
curl -X POST "https://api.apify.com/v2/acts/pharaoh_data~gulf-jobs-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sources":[{"boardUrl":"https://jobs.ashbyhq.com/ziina","companyName":"Ziina"}],"outputMode":"changes","monitorId":"ziina-watch"}'
```

JavaScript (`npm install apify-client`):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('pharaoh_data/gulf-jobs-monitor').call({
    sources: [{ boardUrl: 'https://apply.workable.com/foodics/', companyName: 'Foodics' }],
    outputMode: 'changes',
    monitorId: 'foodics-watch',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const summary = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('HIRING_SUMMARY');
```

Python (`pip install apify-client`):

```python
import os

from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("pharaoh_data/gulf-jobs-monitor").call(run_input={
    "sources": [{"boardUrl": "https://jobs.lever.co/kpler", "companyName": "Kpler"}],
    "countries": ["AE", "SA"],
    "outputMode": "changes",
})
new_jobs = [job for job in client.dataset(run["defaultDatasetId"]).iterate_items() if job["changeType"] == "new"]
```

- **n8n / Make / Zapier:** use the Apify integration's "Run Actor" action (or an HTTP request to the endpoint above), then filter on `changeType`.
- **Alerts:** add an Apify webhook on "Run succeeded" that posts the dataset to Slack, email, or your CRM.
- **AI agents (MCP):** connect `https://mcp.apify.com/?actors=pharaoh_data/gulf-jobs-monitor` so an agent can call the Actor as a tool.

### How change tracking works

- History is kept per **monitor** in the named key-value store `gulf-jobs-monitor-state` in your Apify account, under `monitor-<monitorId>`. Use a different `monitorId` for each independent watch list.
- A job is remembered only after its record is saved. If a run stops early or reaches `maxResults` or your spending limit, unsaved jobs are reported on the next run.
- **Closed** means a job an earlier run saved is no longer listed by a board that was read successfully in this run. A board that fails never produces false closures; its history is kept.
- A job that is still listed but no longer matches your filters is dropped from the history silently, not reported as closed. Removing a board from the input forgets its jobs.
- **Updated** compares title, location, department, employment type, workplace type, salary, and description (descriptions only when both runs collected them).

### Coverage, limits, and responsible use

- Supported: Greenhouse Job Board API, Lever Postings API (US and EU), Ashby's public job posting API, and Workable's public jobs widget endpoint, which Workable's own careers-page widget uses. These are all public, unauthenticated endpoints that employers publish so their jobs can be listed.
- **Not supported:** SmartRecruiters (its API host's robots.txt disallows general crawlers), Workday, Oracle/Taleo, SuccessFactors, LinkedIn, Bayt, and other job boards. No proxies, logins, or CAPTCHA solving are used.
- Requests are sequential, with at least 0.5 seconds between requests to the same host and 1 second for Lever (Lever's published crawl delay). They time out after 30 seconds, retry at most twice on rate limits or server errors, and responses are capped at 25 MB per board.
- Up to 100 boards and 10,000 records per run; Lever boards are paginated up to 10,000 listings.
- Coverage depends on what each employer publishes. Some boards omit departments, dates, workplace type, or salary. Greenhouse publishes employment and workplace type only as optional custom fields.
- Job descriptions are the employer's content. Check the employer's terms and applicable rules (including Saudi PDPL, UAE PDPL, and GDPR when relevant) before storing or redistributing them. Workable signals `ai-train=no` for its pages, so do not use Workable content to train AI models.

### Pricing

This Actor uses pay-per-event pricing: you pay per **job result** (one record in the dataset, including closed-job records), plus Apify's small Actor start event. If the Pricing tab also lists a **Job change** event, records saved in "Changes only" mode are billed at that price instead. Your Apify plan's prepaid usage applies. Check the **Pricing** tab for current amounts. "Changes only" mode keeps scheduled runs cheap because unchanged jobs are not saved or charged.

Typical runs finish in seconds with 512 MB of memory.

### FAQ

**Does it find employers for me?**
No. You provide the boards. To find a company's board, open its careers page and look for a `jobs.lever.co`, `greenhouse.io`, `jobs.ashbyhq.com`, or `workable.com` link.

**Why did a board fail?**
Usually because the board name in the URL is wrong or the board is not public. The run log and `HIRING_SUMMARY` show the error for each board, and other boards still run.

**Why do I see the same jobs as `new` again?**
`monitorId` changed. That happens automatically if you edit boards or filters without setting a fixed `monitorId`, or if earlier runs stopped at `maxResults` before saving them.

**Can I get only Saudi jobs from a global company?**
Yes. Set `countries` to `["SA"]`. Listings in London or New York are dropped; multi-location listings are kept if any location is in Saudi Arabia.

**Is Arabic supported?**
Yes, for matching and for country detection. Titles and descriptions are returned in whatever language the employer used.

# Actor input Schema

## `sources` (type: `array`):

Public careers-board root URLs on Greenhouse, Lever, Ashby, or Workable, for example https://jobs.lever.co/company, https://job-boards.greenhouse.io/company, https://jobs.ashbyhq.com/company, or https://apply.workable.com/company/. The provider is detected from the URL. Optional per board: companyName (label in results) and country (used for listings whose location names no place, such as "Remote"). Use only boards whose terms permit your intended use. Up to 100 boards.

## `countries` (type: `array`):

Keep listings located in these GCC countries, detected from the city or country in each listing's location (English or Arabic). Listings with no specific location use the board's fallback country. Leave empty to keep listings from every location worldwide.

## `jobTitles` (type: `array`):

Optional phrases to match in job titles, such as "engineer" or "مدير". Any phrase can match.

## `locations` (type: `array`):

Optional location terms such as Riyadh, Jeddah, Dubai, Doha, or Abu Dhabi. Any term can match.

## `keywords` (type: `array`):

Optional terms to match in the title, description, department, company, or location. Any term can match. Arabic spelling variants (أ/إ/ا, ى/ي, ة/ه) and diacritics are ignored.

## `postedWithinDays` (type: `integer`):

Optional age filter using each provider's published or created date. Use 0 for no date filter. Listings without a usable date are kept.

## `maxResults` (type: `integer`):

Maximum dataset records to save in one run, including closed-job records (1–10,000). Each record is one billable result.

## `trackChanges` (type: `boolean`):

Label each job new, updated, or unchanged compared with the previous run of this monitor, and report jobs that were removed from their board as closed. History is kept in your account's named key-value store gulf-jobs-monitor-state.

## `outputMode` (type: `string`):

"All current jobs" saves every matching job with its change status. "Changes only" saves only new, updated, and closed jobs, which suits scheduled alerts and keeps costs low. Requires change tracking.

## `includeClosed` (type: `boolean`):

Save a record with changeType "closed" when a job that an earlier run saved is no longer listed by a board that was read successfully in this run.

## `monitorId` (type: `string`):

Optional name for this monitor's change history, such as "riyadh-fintech". Letters, digits, and dashes. When empty, an ID is derived from the boards and filters, so changing them starts a fresh history.

## `includeDescriptions` (type: `boolean`):

Include the plain-text job description (up to 20,000 characters). Turn off for smaller, cheaper-to-store records.

## `redactContactDetails` (type: `boolean`):

Remove email addresses, phone numbers, and personal LinkedIn profile links that employers sometimes put in descriptions. Recommended unless you have a lawful basis to process them.

## Actor input object example

```json
{
  "sources": [
    {
      "boardUrl": "https://apply.workable.com/foodics/",
      "companyName": "Foodics",
      "country": "SA"
    },
    {
      "boardUrl": "https://jobs.ashbyhq.com/ziina",
      "companyName": "Ziina",
      "country": "AE"
    },
    {
      "boardUrl": "https://job-boards.eu.greenhouse.io/tamara",
      "companyName": "Tamara",
      "country": "SA"
    },
    {
      "boardUrl": "https://jobs.lever.co/kpler",
      "companyName": "Kpler",
      "country": "AE"
    }
  ],
  "countries": [
    "SA",
    "AE",
    "QA",
    "KW",
    "BH",
    "OM"
  ],
  "jobTitles": [],
  "locations": [],
  "keywords": [],
  "postedWithinDays": 0,
  "maxResults": 1000,
  "trackChanges": true,
  "outputMode": "all",
  "includeClosed": true,
  "monitorId": "",
  "includeDescriptions": true,
  "redactContactDetails": true
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset of matching job records, each with changeType new, updated, unchanged, or closed.

## `hiringSummary` (type: `string`):

Per-company counts of matching, new, updated, and closed jobs, by country and top departments.

# 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 = {
    "sources": [
        {
            "boardUrl": "https://apply.workable.com/foodics/",
            "companyName": "Foodics",
            "country": "SA"
        },
        {
            "boardUrl": "https://jobs.ashbyhq.com/ziina",
            "companyName": "Ziina",
            "country": "AE"
        },
        {
            "boardUrl": "https://job-boards.eu.greenhouse.io/tamara",
            "companyName": "Tamara",
            "country": "SA"
        },
        {
            "boardUrl": "https://jobs.lever.co/kpler",
            "companyName": "Kpler",
            "country": "AE"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pharaoh_data/gulf-jobs-monitor").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 = { "sources": [
        {
            "boardUrl": "https://apply.workable.com/foodics/",
            "companyName": "Foodics",
            "country": "SA",
        },
        {
            "boardUrl": "https://jobs.ashbyhq.com/ziina",
            "companyName": "Ziina",
            "country": "AE",
        },
        {
            "boardUrl": "https://job-boards.eu.greenhouse.io/tamara",
            "companyName": "Tamara",
            "country": "SA",
        },
        {
            "boardUrl": "https://jobs.lever.co/kpler",
            "companyName": "Kpler",
            "country": "AE",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("pharaoh_data/gulf-jobs-monitor").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 '{
  "sources": [
    {
      "boardUrl": "https://apply.workable.com/foodics/",
      "companyName": "Foodics",
      "country": "SA"
    },
    {
      "boardUrl": "https://jobs.ashbyhq.com/ziina",
      "companyName": "Ziina",
      "country": "AE"
    },
    {
      "boardUrl": "https://job-boards.eu.greenhouse.io/tamara",
      "companyName": "Tamara",
      "country": "SA"
    },
    {
      "boardUrl": "https://jobs.lever.co/kpler",
      "companyName": "Kpler",
      "country": "AE"
    }
  ]
}' |
apify call pharaoh_data/gulf-jobs-monitor --silent --output-dataset

```

## MCP server setup

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

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/fcBWMsaWsuK7mEDzK/builds/EewgxiO3F9Sxdwwlm/openapi.json
