# JobStreet Jobs Scraper (`automation-lab/jobstreet-jobs-scraper`) Actor

Extract public JobStreet jobs in Malaysia, Singapore, Indonesia and the Philippines. Search by keyword and location and export titles, employers, salary labels, dates and vacancy summaries for recruiting and labor-market datasets.

- **URL**: https://apify.com/automation-lab/jobstreet-jobs-scraper.md
- **Developed by:** [Automation Lab](https://apify.com/automation-lab) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.48 / 1,000 item extracteds

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

## JobStreet Jobs Scraper

Extract public **JobStreet jobs** from Malaysia, Singapore, Indonesia and the Philippines into a structured vacancy dataset. Search by keyword and location, then export job IDs, titles, employers, source salary labels, posting dates and short summaries for recruiting research and labor-market analysis.

This Actor collects search-result records. It does not access private applied jobs, jobseeker accounts or applicant data, and it does not fetch full advert descriptions.

### Why use this Actor?

Recruitment analysts can refresh regional vacancy lists without managing a browser or copying search cards manually. Stable job IDs make it practical to join successive exports in your own database. Salary labels remain exactly as the source presents them: no guessed currency, pay period or missing salaries.

Use it for a bounded country search, not a census of every vacancy. You control the maximum unique jobs per run and can export the default dataset as JSON, CSV or Excel through Apify.

### Who is it for?

- Recruiting teams building shortlists of advertised employer demand.
- Labor-market researchers comparing public vacancy titles and classifications over time.
- Data engineers loading recurring country-level search extracts into a warehouse.

Scheduling and cross-run comparisons are downstream workflows. This Actor does not maintain saved-search state or send alerts itself.

### Supported countries

| Code | Country | Site |
|---|---|---|
| MY | Malaysia | my.jobstreet.com |
| SG | Singapore | sg.jobstreet.com |
| ID | Indonesia | id.jobstreet.com |
| PH | Philippines | ph.jobstreet.com |

Choose one country per run. Use separate runs for several countries; location text alone does not change the selected site. JobsDB Hong Kong/Thailand, SEEK Australia, USA and signed-in JobStreet services are not included.

### Quick start

1. Open the Actor and select a country.
2. Enter a keyword such as `engineer` and optionally a location in that country.
3. Set a maximum number of jobs from 1 to 1000.
4. Start the Actor and wait for a successful terminal status.
5. Open the Jobs dataset and export your preferred format.

The default input searches Malaysia for `engineer` and returns up to 100 unique records.

```json
{
  "country": "MY",
  "keyword": "engineer",
  "location": "",
  "maxItems": 100,
  "sortBy": "relevance"
}
```

### Input parameters

| Parameter | Default | Behavior |
|---|---|---|
| `country` | `MY` | One of MY, SG, ID, PH; uppercase codes only. |
| `keyword` | `engineer` | Source relevance search across vacancy text, not exact phrase/title equality. Empty string searches all jobs. |
| `location` | empty | Country-specific text passed to source geographic search; empty means country-wide. |
| `maxItems` | `100` | Global maximum unique job records per run, inclusive range 1–1000. |
| `sortBy` | `relevance` | `relevance` or `date` (newest first according to source ranking). |

Keyword and location whitespace is trimmed. Search matching, synonyms, language handling and geographic boundaries are controlled by JobStreet, not local substring filters. A search for engineer can include related roles whose title does not contain that word. There are no multiple-keyword ANY/ALL or exact-phrase modes.

Unknown fields and unsupported values fail validation rather than being silently ignored. Explicit job/search URLs are not input modes.

### Output fields

| Field | Meaning |
|---|---|
| `jobId` | Source numeric vacancy identifier, represented as a string. |
| `url` | Public country-site job URL. |
| `country` | Selected country code. |
| `title` | Public vacancy title. |
| `company` | Employer display name; may be Private Advertiser or null. |
| `locations` | Array of public location labels. |
| `listingDate` | Source posting timestamp, when present. |
| `listingDateDisplay` | Source relative-date label. |
| `teaser` | Short search summary, not full advert text. |
| `salaryLabel` | Source salary label, null if absent or blank. |
| `classifications` | Source classification names. |
| `subclassifications` | More specific source categories. |
| `workTypes` | Employment type labels. |
| `workArrangements` | Source labels such as On-site, when provided. |
| `bulletPoints` | Public search-card highlights. |
| `scrapedAt` | UTC collection timestamp. |

Optional scalar values are null; absent lists are empty arrays. No tracking tokens, raw source bodies or private applicant information are included.

### Example result

This shortened public record was observed during a Malaysia engineer search; fields below reflect the actual output shape. The dataset also contains classifications, dates, highlights and work arrangements when supplied.

```json
{
  "jobId": "95239788",
  "url": "https://my.jobstreet.com/job/95239788",
  "country": "MY",
  "title": "Engineers (Project/Quality/Process/Production/Maintenance/Electrical)",
  "company": "EPMB",
  "locations": ["Shah Alam, Selangor"],
  "salaryLabel": null,
  "teaser": "Join a major new project in Melaka.  Hiring Project, Quality, Process, Production, Maintenance, Electrical, Mechanical & Utilities Engineers."
}
```

### How much does it cost to extract JobStreet jobs?

Billing consists of a **$0.005 one-time start event** per valid run plus one item event for each unique vacancy saved to the dataset. Duplicate or rejected records have no item charge. Valid searches with no jobs have only the start charge. Invalid inputs fail before the start charge.

| Apify plan tier | Per job | 1,000 jobs plus start |
|---|---:|---:|
| FREE | $0.00092 | $0.925 |
| BRONZE | $0.0008 | $0.805 |
| SILVER | $0.000624 | $0.629 |
| GOLD / PLATINUM / DIAMOND | $0.00048 | $0.485 |

BRONZE examples including the start fee:

| Jobs returned | Estimated total |
|---|---:|
| 10 | $0.013 |
| 100 | $0.085 |
| 1,000 | $0.805 |

There is no separate charge for salary labels, categories or summaries. Set the maximum charge in Apify to control spend; a billing cap may stop delivery before the requested result limit. Check the Pricing tab for the current applicable prices.

### Limits and completeness

The Actor requests sequential pages of up to 100 source records and deduplicates by job ID within a run. It stops at `maxItems`, empty results, source-reported exhaustion or a repeated page with no new jobs. A 20-page safety bound prevents runaway pagination; reaching that bound without exhaustion fails the run rather than claiming full completion.

Zero and unlimited limits are not supported. Search indexes can change while pages are collected, and sponsored listings or ranking can move between pages. The maximum is a ceiling, not a guarantee of that many results, relevance or exhaustive country coverage.

### Reliability and failure behavior

Only anonymous direct JSON search is enabled. No login, user cookies, paid proxy or browser input is required. Temporary network failures, rate limits and server errors receive at most two retries with bounded backoff; stable 4xx, non-JSON challenge pages and invalid response shapes fail clearly.

If a later page fails, previously saved rows remain available but are **partial**. Do not treat a failed run as a complete dataset. A successful empty search is a legitimate no-result response, not a hidden access error.

### Integrations

- Schedule one Apify run per country and load its dataset into a spreadsheet or warehouse.
- Join `jobId` across successful runs to detect additions and removals in your own pipeline.
- Use Apify dataset exports or integrations with Make, Zapier or Google Sheets to pass records downstream.
- Compare `salaryLabel` cautiously: absent salary is not zero, and labels can use different currencies or periods.

Store complete exports outside an LLM context when processing large datasets. This Actor does not provide automatic incremental updates, email alerts or historical backfill.

### API usage

Start a run, retain its ID, check terminal status, and retrieve its default dataset. Replace the example environment variable with your own Apify token; never share it in public input.

#### cURL

```bash
curl -X POST 'https://api.apify.com/v2/acts/automation-lab~jobstreet-jobs-scraper/runs' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"country":"SG","keyword":"engineer","maxItems":100}'
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/jobstreet-jobs-scraper').call({
  country: 'MY', keyword: 'engineer', maxItems: 100
});
if (run.status !== 'SUCCEEDED') throw new Error(`Incomplete run ${run.id}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems({ limit: 100 });
console.log(items);
```

#### Python

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/jobstreet-jobs-scraper').call(
    run_input={'country': 'PH', 'keyword': 'engineer', 'maxItems': 100})
if run['status'] != 'SUCCEEDED':
    raise RuntimeError('Incomplete run: ' + run['id'])
print(client.dataset(run['defaultDatasetId']).list_items(limit=100).items)
```

### Use with MCP

Connect an Actor-specific subset rather than a changing broad category:

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=automation-lab/jobstreet-jobs-scraper"
```

For Claude Desktop, Cursor, and VS Code HTTP-compatible clients:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=automation-lab/jobstreet-jobs-scraper"
    }
  }
}
```

Authenticate through your client's supported Apify flow. Discover real tool names and argument schemas with scoped `tools/list`. Actor selection also exposes run/data helper tools; it does not expose exactly one tool. Discovery is read-only, not permission to execute or abort.

Example prompt: “Collect up to 25 public engineering jobs in Malaysia. Return titles, employers, salary labels and source URLs; mark missing salary as unavailable.”

For an authorized invocation, start once and retain the run/status/storage IDs. Those IDs are not result rows. `call-actor` waits at most 0–45 seconds (`waitSecs`, default 30). If nonterminal, follow the **same run** with `get-actor-run`, using bounded backoff such as 2, 4, 8 seconds capped at 10 and a total 120-second deadline. Set client timeout above the chosen server wait plus transport margin. After client timeout, recover the known run instead of starting another. If the ID is unknown, report uncertainty. At deadline report pending; on terminal failure report partial results honestly.

After success, use `get-dataset-items` with the returned `defaultDatasetId`, explicit limit/offset and required fields. A useful consumer budget is pages of 20 untransformed rows, at most 100 rows and 64 KiB serialized UTF-8 admitted to model context, whichever comes first. Enforce the byte limit host-side before model injection; disclose omitted/oversized rows. If the client cannot intercept oversized responses, do not claim a hard byte guarantee. Track source offsets and report continuation offset/truncation rather than claiming completeness. Keep full exports in storage. These are consumer context limits, not limits imposed on collection. Tool-discovery bytes and caching behavior vary by client; no numeric token-saving claim is made.

### Legality and responsible use

Collect only public vacancy information for lawful purposes, respect source terms and applicable privacy/data rights, and avoid excessive repeated searches. Public availability is not a license for unrestricted redistribution. This Actor is independent and is not affiliated with JobStreet or SEEK.

The Actor uses no AI at runtime and sends no vacancy content to model providers. Actor datasets and logs follow your Apify account retention settings and can be deleted through Apify; no cross-run cache is kept. For help, open an Apify Store issue with the run link and non-sensitive reproduction details.

Failed operations send sanitized diagnostic input, exceptions and Actor/build/run IDs to our private GlitchTip service for repair. Secret fields and URL queries are removed; reports are retained for 30 days. Do not submit personal applicant data in input.

### FAQ and troubleshooting

**Why is a salary missing?** The source does not publish salary for every listing. Missing values are null; no salary is inferred.

**Why are results not exact keyword matches?** JobStreet uses relevance matching across vacancy text and related terms. The Actor does not add local title-only matching.

**Why did I get fewer jobs than the maximum?** The search may be exhausted, deduplicated or repeated; an Apify billing cap can also truncate delivery. Check the run log and status.

**What if I receive HTTP 403 or a challenge error?** The source blocked that route. The run fails instead of inventing empty output. Contact the maintainer with the run link; this product has no automatic paid fallback.

**Can I obtain descriptions or applications?** No. This Actor returns public search summaries only, not full adverts, applicant information or applied-job history.

### Related workflows

This is a standalone four-country vacancy-search product. Use Apify scheduling and dataset export for downstream refresh pipelines. No separate related Actor is required for the documented workflow.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation-lab/jobstreet-jobs-scraper/changelog.md

# Actor input Schema

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

Search one JobStreet country per run: Malaysia, Singapore, Indonesia or Philippines.

## `keyword` (type: `string`):

JobStreet relevance search across vacancy text, not an exact phrase or title-only match. Surrounding whitespace is trimmed. Empty string searches all jobs. Omitted value defaults to engineer.

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

Country-specific location text passed to JobStreet location search; geographic matching is decided by the source, not exact label equality. Trimmed; empty searches the whole country. Does not select the country.

## `maxItems` (type: `integer`):

Global maximum unique jobs in this run, 1–1000 (default 100). Zero/unlimited are unsupported. Stops earlier at source exhaustion or repeated pages. Pages contain up to 100 jobs; deduplication may reduce output.

## `sortBy` (type: `string`):

Source order: relevance to keyword or newest listing first. Sponsored/relevance ranking may affect order; no completeness guarantee for changing search results.

## Actor input object example

```json
{
  "country": "MY",
  "keyword": "engineer",
  "location": "",
  "maxItems": 20,
  "sortBy": "relevance"
}
```

# Actor output Schema

## `dataset` (type: `string`):

Default dataset containing unique public vacancies from this run.

# 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 = {
    "keyword": "engineer",
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/jobstreet-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 = {
    "keyword": "engineer",
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/jobstreet-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 '{
  "keyword": "engineer",
  "maxItems": 20
}' |
apify call automation-lab/jobstreet-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/jobstreet-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/2eRfhy4EDih3yCTW0/builds/HHkNfiA21E9Hr81xf/openapi.json
