# MyCareersFuture Scraper: Singapore Jobs, Salaries, UEN (`sol-arkis/mycareersfuture-jobs-monitor`) Actor

Pull Singapore job listings from MyCareersFuture with salary, skills, company and UEN. Search, scrape URLs, or watch for new posts.

- **URL**: https://apify.com/sol-arkis/mycareersfuture-jobs-monitor.md
- **Developed by:** [Arkis](https://apify.com/sol-arkis) (community)
- **Categories:** Jobs, Lead generation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## MyCareersFuture Jobs + Monitor

This Actor pulls job listings from MyCareersFuture, Singapore's government jobs board. Every result is a full job record: title, company, UEN, SGD salary range, skills, the full description text, closing date, industry (SSIC) code, vacancies, years of experience, education level, region and map coordinates. You can search by keyword, scrape a list of job URLs, or run it on a schedule to return only new listings since the last run. It works over plain HTTP, so runs are fast and cheap. Pricing is $0.002 per job returned, plus the small platform Actor start fee.

Two optional add-ons are off by default: **job change alerts** for monitor runs, and **company profiles** for the employers in your results. Each costs $0.002 per item it produces. If you leave them off, nothing changes.

### Use cases

#### 1. Recruiter and BD hiring signals

Recruiters can see who's hiring for a role this week. BD teams can match new openings to their CRM by UEN.

A company that posts many jobs, or suddenly starts hiring for a new function, is a good sales or recruiting lead. Run a scheduled monitor for a category (for example `Information Technology` with `positionLevels: ["Manager", "Middle Management"]`) or for one competitor's UEN. Send the new jobs to your CRM or Slack through an Apify integration. The `companyUen`, `ssicCode`/`ssicDescription` (industry), `companyWebsite`, `vacancies`, `views` and `numberOfApplications` fields help you rank leads.

#### 2. Salary research and talent mapping

Pull every posting for a role family (`search: "data engineer"`, `maxItems: 5000`). Then compare `salaryMin`/`salaryMax` by `positionLevel`, `employmentType`, `educationLevel`, `minimumYearsExperience`, `region` and `skills`. MyCareersFuture requires employers to state a salary range, which makes it a useful public source of Singapore pay data.

#### 3. Job alerts

Job seekers and career coaches can schedule `mode: "monitor"` hourly or daily with their own filters. Each run outputs only postings that are newer than the previous run. Connect the dataset to email, Telegram or a webhook and you have your own job alert.

### Input examples

**Search with filters**

```json
{
  "mode": "search",
  "search": "product designer",
  "categories": ["Design"],
  "employmentTypes": ["Full Time", "Permanent"],
  "minSalary": 5000,
  "maxSalary": 12000,
  "maxItems": 300
}
```

**One employer's jobs**

```json
{ "mode": "search", "postedCompanyUen": "200310022N", "maxItems": 1000 }
```

**Specific job URLs, with the raw description HTML**

```json
{
  "mode": "urls",
  "startUrls": [
    { "url": "https://www.mycareersfuture.gov.sg/job/information-technology/java-full-stack-developer-digiworld-technologies-ed8e29d03d2ebf3280cc359c745bc20a" }
  ],
  "includeDescription": true
}
```

You only need `includeDescription` if you want the `descriptionHtml` field. Every result already has `descriptionText` without it.

**Monitor (schedule this)**

```json
{
  "mode": "monitor",
  "search": "software engineer",
  "positionLevels": ["Senior Executive", "Manager"],
  "postedAfter": "2026-11-01",
  "maxItems": 500
}
```

**Monitor with job change alerts and company profiles (both paid add-ons)**

```json
{
  "mode": "monitor",
  "postedCompanyUen": "200310022N",
  "maxItems": 500,
  "emitJobChanges": true,
  "enrichCompanyProfiles": true
}
```

#### Input reference

| Field | Type | Notes |
|---|---|---|
| `mode` | `search` | `urls` | `monitor` | Default `search`. |
| `search` | string | Free-text keywords. Leave empty to get all jobs, newest first. |
| `categories`, `employmentTypes`, `positionLevels` | string\[] | Exact MyCareersFuture labels, e.g. `"Information Technology"`, `"Full Time"`, `"Professional"`. |
| `minSalary` / `maxSalary` | integer (SGD/month) | `minSalary` is sent to the API. `maxSalary` is applied by the Actor after fetching. |
| `postedCompanyUen` | string | Employer UEN. |
| `startUrls` | URL list | For `urls` mode. The 32-hex job id at the end of each URL is used. |
| `maxItems` | integer | Default 500. With `emitJobChanges`, counts new jobs and change items together. Company profiles don't count. |
| `postedAfter` | ISO date | Keeps only jobs posted on or after this date (day precision, Singapore time). |
| `includeDescription` | boolean | Default `false`. Only adds the raw `descriptionHtml` field. Every job already gets a detail fetch, so you get the description text, closing date, SSIC code and everything else either way. |
| `emitJobChanges` | boolean | Default `false`. Paid add-on, `monitor` mode only. Also outputs previously seen jobs that changed. See below. |
| `enrichCompanyProfiles` | boolean | Default `false`. Paid add-on, any mode. Saves one company profile per unique UEN to the `companies` dataset. See below. |
| `proxy` | proxy configuration | Optional. The API normally works without a proxy. |

#### How the monitor works

The query parameters (keywords, filters, salary bounds, UEN) are hashed into a key in the named key-value store `mycareersfuture-monitor`. Changing any filter starts a separate history. The key holds:

- the job IDs seen in earlier runs (up to the 20,000 most recent),
- a **watermark**: the newest `postedAt` date covered so far,
- `lastRunAt`: when the monitor last ran.

The monitor only outputs jobs that are **newer than the previous run**. It never returns older backlog.

- **First run (no saved state):** outputs up to `maxItems` of the newest matching jobs and sets the watermark to the newest posting date. If `maxItems` cuts off partway through that day, the Actor keeps reading the rest of that day's listings without outputting them. Those jobs are recorded as seen so the next run doesn't return them as "new". It reads at most 50 pages this way, and these list calls aren't charged.
- **Later runs:** output only jobs that are not in the seen set *and* were posted on or after the watermark date. Results come newest first, so paging stops at the first page where every job is already seen or older than the watermark. Older jobs that were never seen, for example because they fell outside the first run's `maxItems`, are not output.
- **Limits:** if `maxItems` or your spending limit stops a later run before all new jobs are output, the remaining new jobs are not marked as seen. The watermark also isn't moved past them, so the next run delivers them.

`postedAfter` still applies as an extra lower bound. Posting dates are day precision (Singapore time), so a job posted later on the watermark day still counts as new.

### Optional add-ons

Both are off by default. You only pay for them when you turn them on, and only for the items they actually produce.

#### Job change alerts (`emitJobChanges`, $0.002 per changed job)

Search results are sorted by posting date, so a job that gets a pay rise, closes early or is reposted usually doesn't show up near the top again. With `emitJobChanges: true` in `monitor` mode, the Actor:

1. Runs the normal new-job catch-up first (charged as `job`, same as before).
2. Re-fetches the most recently seen jobs for this query by ID. It checks up to `min(1000, maxItems)` of them, newest first, and logs the exact number. Jobs that return 404 are skipped.
3. Compares each one with the snapshot saved last time. The tracked fields are `salaryMin`, `salaryMax`, `salaryType`, `status`, `closingDate`, `repostCount`, `numberOfApplications`, `vacancies`, `editCount` and `title`.
4. Outputs a changed job to the default dataset with `itemType: "job-change"`, a `changes` list, the previous snapshot in `before`, and all the normal job fields. It charges one `job-change` event for each item.

Things to know:

- **Snapshots are stored** in the same monitor key-value record as the seen IDs, capped at the same 20,000 jobs.
- **The first run with the toggle on has no change charges.** No snapshots exist yet, so it only records them. Jobs seen before you turned the toggle on get a free baseline snapshot the first time they're re-checked.
- **`views` is stored but never triggers a change by itself.** It moves on almost every run. `numberOfApplications` does trigger one, so busy postings can show up as changed on most runs. Keep that in mind on hourly schedules.
- **Limits:** `maxItems` and your spending limit cover new jobs and change items together. If a limit stops the run before a change is output, that job keeps its old snapshot, so the change is reported on the next run.
- New jobs get `itemType: "job"` when this toggle is on. Every other field is the same as before.
- The toggle is ignored in `search` and `urls` mode.

Example change item (job fields shortened):

```json
{
  "itemType": "job-change",
  "changes": [
    { "field": "salaryMax", "before": 6000, "after": 7000 },
    { "field": "closingDate", "before": "2026-11-19", "after": "2026-12-03" }
  ],
  "before": { "title": "Java Full-Stack Developer", "salaryMin": 3000, "salaryMax": 6000, "status": "Open", "closingDate": "2026-11-19", "numberOfApplications": 12, "views": 140 },
  "id": "ed8e29d03d2ebf3280cc359c745bc20a",
  "title": "Java Full-Stack Developer",
  "salaryMax": 7000,
  "closingDate": "2026-12-03",
  "...": "all other job fields"
}
```

#### Company profiles (`enrichCompanyProfiles`, $0.002 per unique company per run)

After the jobs are processed, the Actor takes the unique employer UENs from the jobs it output this run. It uses the hiring company's UEN, or the posting company's UEN if there isn't one. It fetches each company's MyCareersFuture profile once and saves it to a separate **named dataset called `companies`**, so your job dataset keeps the same shape.

Each record has `itemType: "company-profile"`, `uen`, `name`, `descriptionText`, `descriptionHtml`, `website`, `employeeCount`, `ssicCode`/`ssicDescription` (SSIC 2020 when available), `logoUrl`, `registeredAddress` (formatted from the address with purpose `registered`), the raw `addresses` list, `lastSyncDate`, `jobsInRun` (how many of this run's output jobs came from that company), `source` and `scrapedAt`.

- Charged once per UEN per run, not once per job. A 404 or failed lookup isn't charged.
- It respects your spending limit and logs how many companies were enriched.
- The data is company-level only, taken from the public MCF profile. **No contact details or personal data are collected.**
- The `companies` dataset is a named dataset, so it stays in your account and **collects records across runs**. Find it under Storage → Datasets in Apify Console, or call `GET /v2/datasets/{your-username}~companies/items`. Each record has a `scrapedAt` timestamp, so you can deduplicate by `uen`.

### Output

Each dataset item is one full job record. Search, monitor and URL modes all fetch every job's detail page, so you get the full set of fields by default. Sample:

```json
{
  "id": "ed8e29d03d2ebf3280cc359c745bc20a",
  "jobPostId": "MCF-2026-1967545",
  "url": "https://www.mycareersfuture.gov.sg/job/information-technology/java-full-stack-developer-digiworld-technologies-ed8e29d03d2ebf3280cc359c745bc20a",
  "title": "Java Full-Stack Developer",
  "companyName": "DIGIWORLD TECHNOLOGIES PTE. LTD.",
  "companyUen": "202003576E",
  "postedCompanyName": "DIGIWORLD TECHNOLOGIES PTE. LTD.",
  "companyWebsite": "www.digiworld.sg",
  "companyLogoUrl": "https://static.mycareersfuture.gov.sg/images/company/logos/baa07d84dc07725220e9c1495111df86/DIGIWORLD%20TECHNOLOGIES%20PTE.%20LTD..jpg",
  "companyEmployeeCount": null,
  "ssocCode": "21522",
  "occupationId": "OCC014584",
  "ssicCode": "62011",
  "ssicDescription": "DEVELOPMENT OF SOFTWARE AND APPLICATIONS (EXCEPT GAMES AND CYBERSECURITY)",
  "salaryMin": 3000,
  "salaryMax": 6000,
  "salaryCurrency": "SGD",
  "salaryType": "Monthly",
  "isHideSalary": false,
  "employmentType": "Contract",
  "positionLevel": "Professional",
  "categories": ["Information Technology"],
  "skills": ["JSP", "REST APIs development", "Java Applications", "Hibernate Framework", "JPA", "Backend Development"],
  "keySkills": [],
  "minimumYearsExperience": 4,
  "educationLevel": "Bachelor's degree or equivalent",
  "flexibleWorkArrangements": [],
  "workingHours": null,
  "shiftPattern": null,
  "address": "10 UBI CRESCENT, UBI TECHPARK, Singapore 408564",
  "districts": ["D14 Geylang, Eunos"],
  "region": "Central",
  "lat": 1.32627524038924,
  "lng": 103.896130679533,
  "vacancies": 4,
  "postedAt": "2026-11-05",
  "originalPostedAt": "2026-11-05",
  "updatedAt": "2026-11-05T07:11:06.000Z",
  "closingDate": "2026-11-19",
  "numberOfApplications": 0,
  "views": 1,
  "repostCount": 0,
  "editCount": 0,
  "isPostedOnBehalf": false,
  "status": "Open",
  "descriptionText": "We are looking for a Java Full-Stack Developer with 4–6 years of experience in developing web-based applications ...",
  "source": "mycareersfuture",
  "scrapedAt": "2026-11-05T07:34:25.571Z"
}
```

`descriptionHtml` (the raw HTML of the description) is added only when `includeDescription` is `true`. All other fields come back by default. When a job is posted on behalf of another employer, `companyName`/`companyUen` refer to the hiring company and `postedCompanyName` is the agency.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Actor start (`apify-actor-start`, charged by the Apify platform) | about $0.01 per GB of memory |
| `job`: charged per job scraped and saved to the dataset | $0.002 |
| `job-change`: optional, per changed job output (`emitJobChanges`, monitor only) | $0.002 |
| `company-profile`: optional, per unique company UEN enriched per run (`enrichCompanyProfiles`) | $0.002 |

1,000 jobs cost about **$2**, plus the start fee. The detail fetch for each job is included in the per-job price. The Actor respects your maximum spend per run and stops cleanly when the limit is reached. A monitor run that finds no new jobs costs only the start fee. If you leave both add-ons off, you are never charged for `job-change` or `company-profile`. With `emitJobChanges` on, re-checking unchanged jobs is free. You pay only for jobs that changed.

#### Output fields

| Field | Description |
|---|---|
| `id`, `jobPostId`, `url` | Job identifiers and the public job page |
| `title`, `categories`, `skills`, `keySkills` | Role, MCF categories, all listed skills, and the skills the employer marked as key |
| `companyName`, `companyUen`, `postedCompanyName` | Hiring company, its UEN, and the posting agency if different |
| `companyWebsite`, `companyLogoUrl`, `companyEmployeeCount` | Company website, logo image URL and headcount (when MCF has them) |
| `ssocCode`, `occupationId` | Occupation code (SSOC) and MCF occupation id |
| `ssicCode`, `ssicDescription` | Industry code (SSIC 2020) and its description |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryType`, `isHideSalary` | Advertised salary range, and whether the employer hides it on the site |
| `employmentType`, `positionLevel`, `minimumYearsExperience`, `educationLevel` | Role terms and requirements (education is decoded from the SSEC code) |
| `flexibleWorkArrangements`, `workingHours`, `shiftPattern` | Working arrangements, when stated |
| `address`, `districts`, `region`, `lat`, `lng` | Location, district, region and map coordinates |
| `vacancies` | Number of openings |
| `postedAt`, `originalPostedAt`, `updatedAt`, `closingDate`, `status` | Posting dates and status |
| `numberOfApplications`, `views`, `repostCount`, `editCount` | Engagement and posting history reported by MCF |
| `isPostedOnBehalf` | `true` when an agency posted on behalf of the hiring company |
| `descriptionText`, `descriptionHtml` | Job description as plain text (always), and raw HTML (only with `includeDescription`) |
| `source`, `scrapedAt` | Provenance |

### Notes

- Every job gets a detail request. Up to about 6 run at a time, with exponential backoff on HTTP 429 and 5xx responses.
- Results are deduplicated by job ID within a run.

### Legal

This Actor reads publicly available job listings. It does not log in or access private data. It is not affiliated with MyCareersFuture, Workforce Singapore or the Singapore Government. You are responsible for making sure your use of the data complies with the MyCareersFuture terms of use, Singapore's PDPA and any other law that applies to you.

### Support

Found a bug or need a field that is missing? Open an issue on the Actor's Issues tab.

# Actor input Schema

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

search: scrape jobs matching the filters (or all newest jobs when no filters). urls: fetch specific job pages. monitor: output only jobs not seen in previous runs of the same query.

## `search` (type: `string`):

Free-text search, e.g. job title or skill. Leave empty to browse all jobs (newest first).

## `categories` (type: `array`):

Exact MyCareersFuture category names, e.g. "Information Technology", "Engineering", "Accounting / Auditing / Taxation", "Logistics / Supply Chain".

## `employmentTypes` (type: `array`):

e.g. "Permanent", "Full Time", "Part Time", "Contract", "Temporary", "Freelance", "Internship/Attachment".

## `positionLevels` (type: `array`):

e.g. "Senior Management", "Middle Management", "Manager", "Professional", "Senior Executive", "Executive", "Junior Executive", "Non-executive", "Fresh/entry level".

## `minSalary` (type: `integer`):

Only jobs whose salary range reaches at least this amount.

## `maxSalary` (type: `integer`):

Filtered client-side: drops jobs whose minimum salary is above this amount.

## `postedCompanyUen` (type: `string`):

Only jobs posted by this company (Singapore UEN, e.g. 200310022N).

## `startUrls` (type: `array`):

MyCareersFuture job page URLs. The 32-character job id at the end of the URL is used.

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

Maximum number of items to output to the default dataset. With emitJobChanges on, this covers new jobs and job-change items combined. Company profiles are not counted.

## `postedAfter` (type: `string`):

ISO date (e.g. 2026-11-01). Only output jobs posted on/after this date. Most useful with monitor mode.

## `includeDescription` (type: `boolean`):

Every job already gets a detail fetch, so results always include descriptionText, closingDate, SSIC code and description, vacancies, years of experience, education level, region, coordinates and more. This toggle ONLY adds the raw descriptionHtml field.

## `emitJobChanges` (type: `boolean`):

Optional paid add-on, OFF by default. Only works with mode "monitor". Re-checks previously seen jobs (the most recent min(1000, maxItems)) and outputs an extra item with itemType "job-change" when salary, status, closing date, applications, reposts, vacancies, edit count or title changed since the last run. Charged $0.002 per changed job (event "job-change"). New jobs are still charged as "job". The first run with this on only records snapshots, so it has no change charges.

## `enrichCompanyProfiles` (type: `boolean`):

Optional paid add-on, OFF by default. Works in any mode. For each unique employer UEN among the jobs output this run, fetches the MyCareersFuture company profile (description, website, headcount, SSIC industry, logo, registered address) into a separate dataset named "companies". Charged $0.002 per unique UEN enriched per run (event "company-profile"). No contact or personal data.

## `proxy` (type: `object`):

Optional. The API usually works without a proxy.

## Actor input object example

```json
{
  "mode": "search",
  "search": "data analyst",
  "maxItems": 500,
  "includeDescription": false,
  "emitJobChanges": false,
  "enrichCompanyProfiles": false
}
```

# Actor output Schema

## `jobs` (type: `string`):

All scraped job postings as full detail records (id, url, title, companyName, companyUen, companyWebsite, ssicCode, ssicDescription, salaryMin/Max/Currency, employmentType, positionLevel, educationLevel, minimumYearsExperience, vacancies, categories, skills, keySkills, address, districts, region, lat/lng, postedAt, closingDate, views, numberOfApplications, descriptionText, source, scrapedAt, etc.).

## `jobsOverview` (type: `string`):

Tabular overview of title, company, salary, employment type, level, region, posted/closing dates, applications, views and URL.

## `jobChanges` (type: `string`):

Default dataset view for monitor runs with emitJobChanges on: itemType, title, company, changes \[{field, before, after}], salary, status, closing date, applications and URL. New jobs have itemType "job".

## `companies` (type: `string`):

Named dataset "companies" (only with enrichCompanyProfiles on): itemType "company-profile", uen, name, descriptionText, descriptionHtml, website, employeeCount, ssicCode, ssicDescription, logoUrl, registeredAddress, addresses, lastSyncDate, jobsInRun, source, scrapedAt. Find it under Storage → Datasets (named) in Apify Console, or via the API at /v2/datasets/{username}~companies/items. Named datasets persist across runs and accumulate records.

# 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 = {
    "search": "data analyst"
};

// Run the Actor and wait for it to finish
const run = await client.actor("sol-arkis/mycareersfuture-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 = { "search": "data analyst" }

# Run the Actor and wait for it to finish
run = client.actor("sol-arkis/mycareersfuture-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 '{
  "search": "data analyst"
}' |
apify call sol-arkis/mycareersfuture-jobs-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sol-arkis/mycareersfuture-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/ASnYP4THiPrdrbYH5/builds/5A05cJRW9MiW8Gqgh/openapi.json
