# 104 Taiwan Jobs (`stone_jadex/job104-taiwan-jobs`) Actor

- **URL**: https://apify.com/stone_jadex/job104-taiwan-jobs.md
- **Developed by:** [Stone Jade](https://apify.com/stone_jadex) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.63 / 1,000 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

## 104 Taiwan Jobs

Collect public job listings from **104.com.tw** for recruiting research, salary
analysis and hiring-market monitoring. Search multiple keywords, filter by area
and job category, and export unique jobs to an Apify dataset. Optional detail
requests add qualifications, benefits and work conditions. Recruiter contacts are omitted.

The Actor is `job104-taiwan-jobs`, uses Node.js ESM, and defaults to **128 MB**.

### Example results

These values come from the recorded research fixture `list_ps30.json`; they are
examples, not a promise that a job is still available.

| Field | Example |
| --- | --- |
| `jobCode` | `89sn1` |
| `jobNo` | `13893949` |
| `jobName` | `<韌體工程師> Firmware Engineer 物聯網無線模組產業` |
| `custName` | `勁達國際電子股份有限公司` |
| `jobAddrNoDesc` | `新北市中和區` |
| `salaryLow`, `salaryHigh` | `0`, `0` (negotiable in this listing) |
| `url` | `https://www.104.com.tw/job/89sn1` |

Results keep useful native 104 list field names: `jobName`, `custName`, `custNo`,
`appearDate`, `jobCat`, industry, address, coordinates, salary bounds, description,
education codes and remote-work type. Publication dates use ISO `YYYY-MM-DD` in both list and detail fields.
Uncapped salary maxima (`9999999`) become null, with `salaryMaxUnbounded: true`.
`salaryType` preserves the source type code (list `s10`); `salaryUnit` is
`piece`, `hour`, `day`, `month`, `year`, or null for negotiable/unknown types. Zero salary
bounds remain source values; no salary amounts are estimated.

With `fetchDetails=true`, results also contain `header`, `jobDetail`, `condition`,
`welfare` and `company` objects, including salary bounds, work location,
requirements, benefits and job description. Recruiter contact fields and viewer
state are omitted. Email, Taiwan phone patterns and labeled recruiter names in
free text are masked, including when the legacy `includeContact` option is true.
Masking applies only to description, other-condition text, and welfare text.
Codes, IDs, URLs, coordinates and other structured fields retain their source values.
Free-text masking is heuristic and cannot recognize every possible personal detail.
The full output schema is in [.actor/dataset_schema.json](.actor/dataset_schema.json).

### Inputs

| Input | Default | Meaning |
| --- | --- | --- |
| `keywords` | `[]` | Separate OR searches, e.g. `["python", "資料分析"]`; empty searches all jobs. |
| `areaCodes` | `[]` | Ten-digit strings, e.g. `["6001001000"]` for Taipei City; empty is unrestricted. |
| `jobCategoryCodes` | `[]` | Ten-digit strings, e.g. `["2007000000"]` for IT/software; empty is unrestricted. |
| `maxItems` | `50` | Positive integer; global maximum unique saved jobs across all searches. |
| `fetchDetails` | `false` | One extra detail request per new unique job. |
| `detailConcurrency` | `3` | Maximum simultaneous details; drops to 1 for the rest of the run after a 429. |
| `includeContact` | `false` | Deprecated compatibility option; contact output remains disabled. |
| `useProxy` | `"none"` | `"none"` or `"datacenter"`; datacenter uses `Actor.createProxyConfiguration({ groups: [] })`. |

```json
{
  "keywords": ["python"],
  "areaCodes": ["6001001000"],
  "jobCategoryCodes": ["2007000000"],
  "maxItems": 50,
  "fetchDetails": false,
  "includeContact": false,
  "useProxy": "none"
}
```

Public code trees: [areas](https://static.104.com.tw/category-tool/json/Area.json)
and [job categories](https://static.104.com.tw/category-tool/json/JobCat.json).
Copies recorded with the research samples are bundled in `data/`. These support
automatic query splitting without downloading code lists during each run.
Unknown ten-digit codes can be submitted but have no known descendants for splitting.

### Pagination and limits

Every list request uses `pagesize=30`. 104 may return two extra promoted jobs;
all returned rows are considered and deduplicated globally by `jobNo`.
The Actor stops at the response's `lastPage`, including clamped final pages.

104 caps many searches at about 3,000 results. If more jobs are needed after the
first page, a query with `total > lastPage * 30` is split into **area × category** slices. Supplied
code arrays are separated, and capped single branches are recursively refined
using the bundled trees. An unrestricted capped query starts with the eight
area roots × eighteen category roots. Capped children are split into sub-areas
and sub-categories until pageable or no descendants remain.
The global item limit still applies. Parent and slice overlaps are deduplicated.

If a capped leaf or unknown code cannot be refined, the Actor reads its available
pages and records `OUTPUT.cappedQueries` plus a warning. It does not guarantee
exhaustive coverage of these slices. Jobs may change while a run is in progress;
the site does not provide a consistent snapshot.

List requests are serial. Details use up to `detailConcurrency` workers, with
**one second between all request starts**, including retries. After a 429, already
running details drain and subsequent jobs run one at a time.
Only the remaining gap is waited: a response taking over a second adds no sleep.
Robots crawl delays, retry backoff and `Retry-After` add no sleeps. HTTP 403/429
and network failures retry at most twice at the same one-second start gap;
each request has a 30-second timeout. Other HTTP
errors, redirects, malformed payloads or unavailable details fail the run.
A run that saves **zero jobs fails**, including genuinely empty searches, except
when collection stops at the Actor deadline.
Already saved jobs remain available if a later request fails.

When `ACTOR_TIMEOUT_AT` or `Actor.getEnv().timeoutAt` provides a deadline,
collection stops 15 seconds before it. Pending target requests are aborted;
saved dataset items remain available and `OUTPUT` is written before exit.

The final `Detail and memory summary` logs `http429Count` (429 responses only),
`averageDetailMs` (per-job detail elapsed time including waits and retries), and
`memoryPeakRssBytes`. RSS is sampled with `process.memoryUsage().rss` every five
seconds and at run start/end. These metrics are also stored in `OUTPUT`.

`OUTPUT` records item/request counts, list pages, detail requests, deduplicated
rows, retries, rate limits, splits, unrefinable caps, errors, robots disallow hits
and whether the SDK billing limit or timeout margin stopped collection.
The final `Query slice summary` log and `OUTPUT.slices` include each slice's
filters, reported `total`/`lastPage`, fetched pages, rows before dedupe,
new global job numbers after dedupe, saved items, duplicates and completion status.
`totalBeforeDedupe` and `totalAfterDedupe` count all fetched rows and unique
`jobNo` values across the run. They include any unsaved remainder of a page
when `maxItems`, billing or the deadline stops saving; `items` counts saved rows.
`OUTPUT.listRequestStats` records HTTP attempts, response bytes, cumulative and
maximum HTTP elapsed time, gap waits and status counts. Debug logs show each
list attempt's `status`, `bytes`, `elapsedMs` and `waitMs`; a separate page log
shows save time and total page time. The final info summary includes averages,
`listSaveMs` (saving, including optional details) and `listPageElapsedMs`.
Enable debug logging with `APIFY_LOG_LEVEL=DEBUG` to diagnose slow list pages.

### Billing

Each list page's new validated results are submitted in one
`Actor.pushData(arrayOfItems, 'job')` call, using the SDK's combined dataset/event
operation with per-item charging. The batch is deduplicated within the page and
across the run, and capped at the remaining `maxItems` allowance. Duplicates,
empty results and jobs whose requested details fail do not trigger a `job` event.
Collection stops when the SDK trims the batch or reports a billing limit; only
saved rows count toward items. No separate charge call is used.

**Price: $0.000625 per job — $0.625 per 1,000 jobs**, with no run-start fee.
You pay only for jobs actually saved (duplicates, empty results and jobs whose
requested details fail are not charged, as described above), so 1,000 saved
jobs cost you **$0.625 in total**. For comparison, the main 104 scraper on
Apify (corvuslab/taiwan104-jobs-scraper) charges $0.00125 per job plus a
$0.005 run-start fee, so the same 1,000 jobs cost $1.255 there — about twice
as much.

### Run locally

```bash
npm ci
mkdir -p storage/key_value_stores/default
## Save the JSON example above as storage/key_value_stores/default/INPUT.json.
npm start
npm test
```

Local results are under `storage/datasets/default`, with counters in the default
key-value store's `OUTPUT.json`. On Apify, the default dataset can be exported
as JSON, CSV or Excel. Datacenter proxy use requires appropriate Apify proxy
access; direct access is the default. Tests use recorded fixtures and simulated
responses, without live requests or actual billing. See [TESTING.md](TESTING.md).

#### Is it legal to scrape 104.com.tw?

This Actor collects publicly visible job listings without logging in or using
account cookies. It does not access applicants or private member data. Recruiter contact fields are omitted and recognizable contacts in text are masked. You are responsible for having an appropriate legal basis and for
complying with applicable privacy laws, including Taiwan's Personal Data
Protection Act, and the site's terms. Consult legal advice for your specific use.
See Apify's [discussion of web scraping legality](https://blog.apify.com/is-web-scraping-legal/).

104's [robots.txt](https://www.104.com.tw/robots.txt) disallows search paths in the
research findings. Disallow rules produce one warning per run and increment
`OUTPUT.robotsDisallowedHits` for each affected endpoint; the Actor continues
as configured with the one-second request start gap.

This is an independent tool, with no affiliation, endorsement or sponsorship by
104 Corporation. Data is provided as received for research and analysis; respect
third-party rights when using or republishing descriptions.

# Changelog

This Actor's version history is a separate document: https://apify.com/stone_jadex/job104-taiwan-jobs/changelog.md

# Actor input Schema

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

Run a separate search for each keyword and combine unique jobs (OR). An empty array searches all jobs.

## `areaCodes` (type: `array`):

104 area codes as ten-digit strings, e.g. 6001001000 for Taipei City. Empty means unrestricted. Large queries are split using the public area tree.

## `jobCategoryCodes` (type: `array`):

104 category codes as ten-digit strings, e.g. 2007000000 for IT/software. Empty means unrestricted. Large queries are split using the public category tree.

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

Maximum number of new unique jobs to save across all keywords and query slices. Defaults to 50.

## `fetchDetails` (type: `boolean`):

Request each new job's public detail endpoint to add jobDetail, condition, welfare, header and company fields. Adds one request per unique job.

## `includeContact` (type: `boolean`):

Deprecated compatibility option. Contact fields are always omitted and contact text is always masked.

## `useProxy` (type: `string`):

Use direct requests (none) or Apify datacenter proxy created with no named groups. No residential proxies or automatic proxy changes.

## `detailConcurrency` (type: `integer`):

Maximum simultaneous detail requests. Drops to 1 for the rest of the run after HTTP 429; starts remain spaced by one second.

## Actor input object example

```json
{
  "keywords": [],
  "areaCodes": [],
  "jobCategoryCodes": [],
  "maxItems": 50,
  "fetchDetails": false,
  "includeContact": false,
  "useProxy": "none",
  "detailConcurrency": 3
}
```

# Actor output Schema

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

Scraped items in the default dataset.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("stone_jadex/job104-taiwan-jobs").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("stone_jadex/job104-taiwan-jobs").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 '{}' |
apify call stone_jadex/job104-taiwan-jobs --silent --output-dataset

```

## MCP server setup

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

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/5WRl0I83j3aXn1AmG/builds/KyrdvWsYsT7zEBNIc/openapi.json
