# SEEK, JobStreet & JobsDB Job Scraper — 8 Markets (`bluedata/seek-jobstreet-jobsdb-scraper`) Actor

Scrape public job listings from SEEK, JobStreet and JobsDB across 8 APAC markets, one market per run. Export titles, companies, locations and salary fields to JSON, CSV or Excel. Add optional company data and inspect search coverage in RUN_SUMMARY.

- **URL**: https://apify.com/bluedata/seek-jobstreet-jobsdb-scraper.md
- **Developed by:** [BlueData](https://apify.com/bluedata) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 1 total users, 1 monthly users, 98.8% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.50 / 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

### SEEK, JobStreet & JobsDB Job Scraper — 8 Markets

Search public job listings across eight SEEK, JobStreet, and JobsDB markets with
one consistent dataset. Filter by keywords, location, salary, and category;
optionally add company context, descriptions, and application details. Results
include source links and market/currency labels to support hiring research and
cross-market comparison.

Start with a small search in one market. Optional data groups are off unless
selected, and requested groups report observed availability. Field coverage
varies by listing and market; results are not guaranteed to include every job or
every selected field.

### Choose a workflow

- **Regional hiring research:** use the same fields across SEEK, JobStreet, and
  JobsDB, while keeping currency and market labels explicit.
- **Company context:** select company profiles, aggregate reviews, work context,
  and application information without enabling every optional group.
- **Advanced/experimental change tracking:** incremental runs may flag new and
  changed jobs. They are not a guaranteed monitoring or delivery service.
- **AI and automation:** choose compact output and inspect run status before
  treating results as complete.

Salary bands remain in source currencies; the Actor does not convert currencies
or infer missing pay. Applicant counts and login-only insights are not supported.

### What this Actor does

This actor extracts structured job data from all eight SEEK markets —
Australia and New Zealand (SEEK), Singapore, Malaysia, Indonesia, and the
Philippines (JobStreet), Hong Kong and Thailand (JobsDB) — including
structured salary (min/max/currency/period), real posting datetimes, work
types and arrangements, categories, and source links. It supports keyword search, category filters,
location and salary post-filters, and controllable result limits, so you can
run the same query consistently. Opt-in detail enrichment adds full descriptions,
apply links, company pages, classification IDs, and other supported fields;
incremental mode can flag changes between runs.
These are public-source observations, not proof that a job is
still accepting applications or that an employer has officially closed it.

### Key features

- **🌏 Eight-market coverage** — one input shape across AU, NZ, SG, MY, HK,
  TH, ID, and PH, with market-aware currency and locale handling.
- **📦 Normalized output** — consistent fields, source links, and run status
  make results easy to use in applications and Agent workflows.
- **💰 Structured salary** — `salaryMin` / `salaryMax` / `salaryCurrency` /
  `salaryPeriod` when provided by the source (THB, SGD, AUD, NZD, MYR, HKD, IDR,
  PHP), plus a `placeholder` status for implausibly low ranges such as monthly
  "1 - 2 THB". These are parsing heuristics, not evidence of employer intent.
- **📅 Real posting dates** — `postedDate`/`postedDaysAgo` from the true UTC
  timestamp, not a parsed "2 days ago" label.
- **🎯 Category filter** — `categoryIds` narrows results using source
  classification data. IDs can be read from enriched records.
- **📋 Detail enrichment** — toggle `enrichDetails: true` for the full
  description (plain text), apply link, company page, and classification IDs.
- **♻️ Incremental mode** — recurring runs flag `isNew` / `isUpdated` per
  record from cross-run state. First run builds the baseline; later runs
  highlight the diff.
- **🛡️ Output validation** — `contractCheck: true` checks expected fields for
  scheduled monitoring.
- **📦 Compact mode** — smaller payloads for AI-agent and MCP workflows.

Public contact fields are sensitive and remain off by default; enable them
only when needed, and separately allow them in notifications. Data that requires
a logged-in session, including applicant insights, is not accessed and is
rejected if requested. Activity counts describe listing activity and are not
applicant counts. Descriptions and company text use best-effort PII redaction;
this is not a guarantee of anonymization.

### What data can you extract?

Each result includes core listing fields (`jobId`, `title`, `company`,
`canonicalUrl`, `location`, `market`, `source`, `sourceCountry`), structured
salary (`salaryText`, `salaryMin`, `salaryMax`, `salaryCurrency`,
`salaryPeriod`, `salaryParseStatus`), work context (`workType`,
`workArrangement`, `categories`, `teaser`), posting data (`postedAt`,
`postedDaysAgo`, `postedDate`, `fetchedAt`), relevance (`queryTermsMatched`,
`relevanceScore`), provenance (`searchQuery`, `searchUrl`), and — when detail
enrichment is on — `description`, `applyUrl`, `companySearchUrl`,
`classificationIds`, `subClassificationIds`. Optional data groups can add
listing metadata, work and location context, classifications, company profile
and review summaries, public media, application details, public contacts,
listing activity, and source references. Groups are off by default; fields are
omitted when the source does not provide them, with availability reported in
`optionalDataStatus`. Incremental runs add `isNew` / `isUpdated`.

#### Important interpretation limits

- **Visibility is not availability.** Current Actor metadata (`isPublic`) controls
  deployment visibility; the README cannot override it. Public accessibility of
  a source listing is a separate concept and does not confirm application access
  or an open vacancy.
- **A posting date is not an open/closed decision.** `postedDate` and
  `postedAt` describe when the source reported the listing. The Actor does not
  guarantee that the employer is still accepting applications, and a reliable
  closing date is not generally available.
- **Verify links before acting.** Treat `canonicalUrl` as the source citation
  and `applyUrl` as a candidate application/share link. Re-check both at use
  time; a link can redirect, expire, require a login, or no longer represent an
  active vacancy.
- **Read salary status, not numbers alone.** `salaryParseStatus: suspicious`
  nulls questionable numeric bounds while preserving readable `salaryText`.
  Check `salaryWarnings`; these are heuristics, not proof of fraud or pay
  accuracy. `structured` means parseable, not verified by an employer.
- **`isExpired` is a search observation.** It means a previously seen listing
  was absent from a later eligible/full search under the incremental scope. It
  does not mean the employer officially closed the vacancy. It can reflect
  source changes, filters, or temporary absence;
  inspect `RUN_SUMMARY` before acting on it.

### Input

The main inputs are search keywords, a market code, and a result limit.

Key parameters:

- 🔍 **Search**: `query`, `market` (default `TH`, dropdown, 8 markets), `location`,
  `startUrls`, `preset` (Basic / Company context / Full optional data), `sortMode`, and
  `maxResults` (accepted output limit; 0 removes the requested output cap subject
  to safety limits unless `exportExcel` is enabled. Check run status for partial
  coverage).
- 🧩 **Flexible API input**: `query` and `location` accept comma-separated text,
  JSON-string arrays, or native string arrays. Direct API callers may also use
  `{"terms":["software engineer","data analyst"]}` for `query`. `startUrls`
  accepts either URL strings or `{ "url": "..." }` objects. If both `query` and
  `startUrls` are empty, the legacy default query is `AI Engineer`; an empty
  `query` with `startUrls` uses the URL targets without adding that default.
- 📊 **Custom XLSX**: set `exportExcel: true` for a stateless run with
  `maxResults` from 1 through 1000. The requested workbook is available at the
  enabled-only `JOBS.xlsx` output link after a successful collection.
- 🎯 **Filters**: `salaryMin` / `salaryMax` (market home currency, overlap),
  `classification` text, `categoryIds`.
- 🎯 **Post filters**: `includeKeywords` / `excludeKeywords`,
  `keywordMatch*` field toggles, `fromDate` / `toDate` / `maxAgeMinutes`,
  `customFilters` (field/op/value engine).
- 📄 **Enrichment**: choose `preset` first. Basic is fastest; Company context
  enables selected detail groups; Full optional data enables supported public
  groups except contacts/login-only data. Advanced users may still provide
  `enrichDetails`, `descriptionMaxLength`, or `additionalData` explicitly.
- 🧩 **Optional data** (`additionalData.<group>`): `listingMetadata`,
  `workContext`, `locationDetails`, `classifications`, `companyProfile`,
  `companyReviews`, `jobMedia`, `applicationInfo`, `publicContacts`,
  `sourceActivity`, `sourceReferences`, `searchInsights`, and
  `applicantInsights`. Applicant insights are not available for public searches.
  Contact fields are sensitive and are sent in notifications only when explicitly
  allowlisted with `notificationDataGroups`.
- 🤖 **AI output**: `compact`, `excludeEmptyFields`, `contractCheck`.
- ♻️ **Incremental**: `incremental`, `stateStoreName`, `stateKey`, `emitUnchanged`,
  `emitExpired`, `skipReposts` (adds `isNew` / `isUpdated` / `isExpired` /
  `isRepost` flags).
- 🔔 **Notifications**: `webhookUrl` / `webhookHeaders`, `discordWebhookUrl`,
  `notifyOnlyChanges`, `notificationLimit`, `includeRunSummary`.

### Stateless quick start

Start with this small Basic search, which requests up to five software-engineer
listings in Singapore. Standard `market` input selects one market per run; it
does not automatically fan out across all eight markets. Advanced `startUrls`
can select supported hosts and add URL-based searches.

```json
{
  "query": "software engineer",
  "market": "SG",
  "preset": "basic",
  "maxResults": 5
}
```

Optional data groups are off unless selected; public contacts and applicant
insights remain off in both examples.

#### Guidance for AI agents

When choosing inputs programmatically, select exactly one `preset` unless the
user explicitly asks for custom fields:

- `basic`: default choice for fast search; no detail enrichment.
- `company`: use when the user asks for company, work-context, or application
  context; it enables the selected public detail groups.
- `full`: use only when the user asks for the deepest available public data; it
  enables supported public groups but never login-only applicant insights or
  public contacts by default.

Keep `market` to the user's requested market, set `maxResults` to the requested
bounded count, and use `compact: true` for agent-oriented output unless the user
needs full teaser text. Do not send `detailConcurrency`; it is managed by the
Actor. Do not claim completeness from `sourceStatus: partial` or `coverage_limited`, or treat
`not_provided`, `restricted`, `failed`, and `skipped_budget` as equivalent. Read
`RUN_SUMMARY` and `optionalDataStatus` before summarizing results. The standard
input searches one market per run; do not silently fan out across all eight.

#### Preset reference

Presets add enforced defaults; `additionalData` may add more groups but a
preset's groups cannot be turned off with `false`. Choose `preset: "basic"` when
no enrichment is requested. It leaves `enrichDetails` off and enables no optional
groups. `preset: "company"` enables exactly `workContext`, `companyProfile`, and
`applicationInfo`; it is intended for company/work/application context and does
not enable every optional group. Choose Basic instead of Company when those
groups must stay off. `preset: "full"` enables `enrichDetails` by default plus
these public groups:
`listingMetadata`, `workContext`, `locationDetails`, `classifications`,
`companyProfile`, `companyReviews`, `jobMedia`, `applicationInfo`,
`sourceActivity`, `sourceReferences`, and `searchInsights`. Neither preset
turns on `publicContacts` or `applicantInsights`; applicant insights are not
available for public searches. Use explicit `additionalData` or
`enrichDetails` only when the preset choices do not match the user's request.
If a caller explicitly sends `enrichDetails: false` with Full, the optional
preset groups still run but legacy description/apply-link enrichment is disabled.
Preset values are lowercase `basic`, `company`, or `full`.

#### Input formats and location handling

Market selection does not automatically translate Malay or Indonesian query
phrases into English. Submit the exact search terms you want sent to the source.
Location names are not translated arbitrarily. The Actor retains the original
location and reports the effective source location used; location filtering is
not a guarantee of complete geographic coverage.

The saved `INPUT` preserves the original input, `RUN_SUMMARY.locations` reports
canonical values, and `RUN_SUMMARY.inputNotes` records normalization. Each
`searchPlan` entry and processed `searches` entry exposes `originalLocation` (the
value you sent) and `effectiveLocation` (the source label actually used); for
example `originalLocation: "香港"` and `effectiveLocation: "Hong Kong"`. A location
filter is a post-filter, not a guarantee of complete geographic coverage. If the source stops before full coverage, read
`sourceStatus`, `searches[].stopReason`, `searches[].filteredOut`, and
`searches[].coverageComplete` before concluding that no jobs exist. Hit the
output cap and hitting a fetch-window cap are reported separately through
`resultLimitReached` and `coverageLimitReached`.

Accepted API examples:

```json
{
  "query": ["software engineer", "data analyst"],
  "market": "TH",
  "location": "Bangkok",
  "startUrls": ["https://th.jobsdb.com/th/software-engineer-jobs"],
  "maxResults": 10,
  "preset": "basic"
}
```

```json
{
  "query": {"terms": ["software engineer", "data analyst"]},
  "market": "TH",
  "startUrls": [{"url": "https://th.jobsdb.com/th/software-engineer-jobs"}]
}
```

If `RUN_SUMMARY.sourceStatus` is `coverage_limited`, inspect
`coverageLimitedSearches`, `searches[].coverageLimit`, and `filteredBy`. The
result is not a full-market result and is not proof that the source has no other
matching jobs. `coverageLimitReached: true` means the fetch window ended before
the source was exhausted; `resultLimitReached` answers the different question
of whether the requested `maxResults` output cap was met. A run can show
`resultCount` below `maxResults` with `resultLimitReached: false` because the
coverage window was capped or because filters rejected the remaining candidates;
`filteredBy` and `coverageLimit` distinguish those causes.

#### Query matching and language handling

Eight markets does **not** mean every query language is translated or understood.
Queries are not automatically translated; submit the exact phrase you want
searched. Supported query matching is literal evidence, not semantic search or a
language translation service.

Choose `queryMatchMode`:

- `source` (default): preserves source ranking when choosing candidates, but
  only records with complete target-specific literal evidence are emitted.
  Zero-evidence suggestions are filtered before output and never appear with
  `relevanceScore: 0`.
- Non-Latin queries require literal evidence in the selected fields. Thai in TH
  and Chinese in HK are supported as exact text: the original query is sent
  unchanged, with no translation or added synonyms. Complete coverage with no
  literal match returns `no_results` and `languageSupport: supported`; incomplete
  coverage returns `coverage_limited` and `languageSupport: unknown`.
- For the SG source, an Arabic- or Korean-script query whose first result page
  contains candidates but no literal query match is terminally marked
  `unsupported_language` instead of paging through generic fallback results.
  Literal matches on that page remain eligible. This terminal outcome counts as
  completed, but not fully covered; it does not set `truncated`.
- `all_terms`: requires every normalized query token across the selected fields.
  ASCII tokens use word boundaries (`engineer` does not match `engineering`);
  other scripts use literal matching without a linguistic word segmenter.
- `phrase`: requires the contiguous query phrase in a selected field.

The default strict-query fields are title, teaser and actual description.
`keywordMatch*` selects a narrower field set. With no explicit include/exclude
keywords, selecting fields also enables all-token query validation, so
`keywordMatchTitle: true` with `junior software engineer` rejects a title that
only says `Software Developer`. With explicit include/exclude keywords the old
keyword-only scope is preserved; set `queryMatchMode: all_terms` to enforce both.
Description-based query validation waits for requested `enrichDetails` and uses
real description text, not teaser text mislabeled as description.

`matchedTerms` and `matchedFields` are literal evidence, not a semantic score.
Every emitted `relevanceScore` is `1` **for this search target**, meaning all
required token/phrase evidence was found in the original or normalized query.
`queryTermsMatched` names the validated target query, not an incidental match to
a different query in the batch. This is never a confidence probability. Strict
checks can reject relevant synonyms and do not guarantee semantic accuracy.
An empty complete Thai/Chinese exact query is `no_results`; a known unsupported
language/source combination is `unsupported_language`. Configured page/scan or
candidate caps return `coverage_limited`, not `partial`, and keep
`truncated: true` plus `coverageLimit` so consumers know the source was not
fully covered.

```json
{
  "query": "junior software engineer",
  "market": "TH",
  "location": "Bangkok",
  "queryMatchMode": "all_terms",
  "keywordMatchTitle": true,
  "maxResults": 10,
  "preset": "basic"
}
```

#### Search URLs

`startUrls` is optional and additive to explicitly supplied keyword searches. It accepts an array of
HTTPS search/category URL strings or objects such as
`{"url":"https://th.jobsdb.com/th/developer-jobs"}`. URLs must use one of the
8 supported SEEK/JobStreet/JobsDB hosts, must be search/category pages (not a
homepage or detail page), and support at most 20 entries. A URL's host selects
the market for that target; when a URL is supplied without a query, the search
term is derived from its URL slug. If both `query` and `startUrls` are supplied,
both sets of searches run and duplicate normalized URLs are merged. A URL-only
input does not receive a hidden schema-default keyword. `RUN_SUMMARY.searchPlan`
exposes each target URL and `inputSource` (`query`, `startUrl`, or a merged value)
so default-query and URL-duplication cases are distinguishable. `market`
still controls generated keyword searches and remains a useful explicit default.
Example:

```json
{
  "query": "developer",
  "market": "TH",
  "startUrls": [{"url":"https://th.jobsdb.com/th/developer-jobs"}],
  "preset": "basic",
  "maxResults": 25
}
```

#### Filters and field semantics

- `classification` matches text in source category labels; `categoryIds` matches
  source classification IDs and is not a salary or internal database ID. Read
  classification fields from an enriched sample before reusing an ID.
- `includeKeywords` requires every term to match; `excludeKeywords` rejects a
  record when any term matches. With no `keywordMatch*` toggle, matching uses
  the legacy set of title, company, location, work type, teaser, category, and
  description. Selecting one or more `keywordMatchTitle`, `keywordMatchCompany`,
  `keywordMatchLocation`, `keywordMatchCategory`, or
  `keywordMatchDescription` restricts matching to those fields.
- `customFilters` is a JSON array of `{field, op, value}` objects. Supported
  operators are `includes`, `notIncludes`, `equals`, `notEquals`, `gt`, `gte`,
  `lt`, and `lte`. Example: `[ {"field":"salaryMin","op":"gte","value":50000} ]`.
  A filter on an optional group requires that group in `additionalData`; a
  description/apply/classification detail filter requires `enrichDetails` or
  the corresponding optional group.

#### Filter order and missing values

The Actor maps and validates source listings first, then applies location,
keyword, salary, date, classification, and any non-detail `customFilters` before
accepting a candidate. Accepted candidates are deduplicated by job ID and
fetched for requested detail groups. Filters that need detail fields (for
example description or an optional company field) run after enrichment. When
incremental/repost logic is enabled, change flags are computed after the final
record is assembled; formatting such as `compact` happens last. `maxResults`
counts final accepted output records, so deferred filters may inspect more source
candidates or detail records than the output count.

Include/exclude keyword matching is case-insensitive substring matching; unlike
strict query checks, it has no token boundaries, stemming, or language-aware
segmentation. Missing keyword fields behave as empty text. If a
salary threshold is set, records with no structured salary are excluded; records
in a different currency are kept rather than incorrectly converted. Records with
no parseable posting date are kept by date filters. A missing custom-filter field
fails positive operators (`includes`, `equals`, numeric comparisons) and passes
negative operators (`notIncludes`, `notEquals`). Multiple custom filters are
ANDed together; multiple include terms are ANDed and exclude terms are ORed.

#### Salary plausibility

`salaryParseStatus` is `structured`, `placeholder`, `suspicious`, or
`not_provided`. Suspicious heuristics cover implausible lower bounds crossing a
known currency/period floor, ranges of at least 100:1, and multiple numeric bands
in one display label. Example: AUD 1–500,000/year is not treated as a trustworthy
structured range: numeric bounds become null and readable text remains.
Salary thresholds exclude such null-bound records. `salaryWarnings` explains
flags; `foreign_currency` alone keeps a structured range because legitimate
foreign-currency offers exist. No automatic currency conversion is performed.

#### Multi-query execution and the global result cap

`maxResults` is a **run-wide output** cap, not a per-query output quota and not
a guarantee that the source scan reaches every listing. The Actor fetches a
candidate window sized for scanning (at least 100 listings per source page for
bounded runs), because market filters can reject most candidates after fetching.
For a multi-query or multi-URL plan, a best-effort run divides the source
candidate allowance across targets so every planned target gets an execution
opportunity before the final output cap is applied. A `fill_target` run may
intentionally skip later targets once the run-wide output target is met. The
Actor never emits more than `maxResults`;
source request/time budgets, challenges, interruptions, and source scan/page caps
can leave searches incomplete. Detail request limits are reported separately.

`coverageMode` selects how hard the run tries to reach `maxResults`:

- `best_effort` (default): the bounded scan that stops at the standard safety
  limits. Use `RUN_SUMMARY` coverage facts to see whether the target was met.
- `fill_target`: keeps scanning and enriching candidates until `maxResults`
  matched jobs are emitted, the source is exhausted, or a hard run limit
  (global candidate cap, page cap, request budget, or time budget) is reached.
  The page and detail-candidate safety windows are widened for that run; the
  hard caps and the run time budget still apply. Once the run-wide target is
  met, remaining planned targets are skipped with `stopReason: max_results`.
  Pricing is unchanged: only emitted dataset items and the start event are
  charged, and rejected candidates are not charged.

`RUN_SUMMARY` exposes:

- `plannedSearches`: distinct targets after URL/alias deduplication.
- `attemptedSearches`: targets whose source attempt started, including prefetches
  whose results were subsequently discarded. Retries do not inflate this count.
- `processedSearches`: targets executed in this run, excluding skipped,
  discarded, and checkpoint-resumed targets.
- `completedSearches`: target outcomes that reached a terminal result in this
  run, including `coverage_limited` and intentional `maxResults` caps. It
  excludes source request/time/challenge failures and failed attempts.
- `fullyCoveredSearches`: processed targets for which the source reported
  complete pagination. A cap-limited search can be processed and completed while
  remaining not fully covered.
- `coverageLimitedSearches`: processed targets stopped by an explicit source
  page/candidate cap; inspect their `coverageLimit` for the configured window.
- Run-level `resultLimitReached`: final accepted output reached the requested
  `maxResults` cap. `coverageLimitReached`: a source page/candidate window cap
  cut collection short. They are independent. Legacy `limitReached` remains a
  broader cap signal; use the explicit flags to distinguish output limits from
  incomplete source coverage. Per-search flags report whether that target itself
  stopped at either limit. Final run-wide trimming can set the run-level output
  flag while a fully scanned search has `stopReason: null` and per-search
  `resultLimitReached: false`.
- `sourceStatus` and `searches[].status` describe source search only.
  `detailStatus` describes enrichment separately; a detail-request budget
  (`maxDetailRequests`) or detail failure does not make a completed source search
  `partial` or truncated. The separate detail-candidate coverage cap is a source
  coverage limit and can make the run coverage-limited.
- `detailsEnriched` counts valid detail responses per candidate listing, not
  final dataset rows. It can exceed `resultCount` when candidates are filtered
  after enrichment. `detailsFailed` counts unsuccessful detail responses.
- `coverageMode`, `requestedResults`, `targetReached`, and `shortfall` state how
  close the run came to the `maxResults` target; `candidatesScanned` and
  `candidatesRejected` show the work done; run-level `stopReason` explains why
  the run stopped (`target_reached`, `candidate_limit`, `time_limit`,
  `request_limit`, `interrupted`, `challenge`, `source_exhausted`, `early_stop`).
  `resultNotice` is a short human-readable note when the target was not met; a
  `TARGET_NOT_REACHED` warning is added only for `fill_target` runs with a
  shortfall.
- `incrementalStateStatus` is `ready`, `run_scoped`, `unavailable`, or
  `not_requested`. State-load failure suppresses `isNew`/`isUpdated` rather than
  claiming every result is new; check the warning `INCREMENTAL_STATE_UNAVAILABLE`.
- `baselineComplete` and `expiredRecordsReliable` are reported for incremental
  runs. An incomplete coverage window makes the baseline partial: expiration
  markers are suppressed, `expiredRecords` stays `0`, and the
  `BASELINE_INCOMPLETE` warning explains why.
- `searches[].coverageLimit` identifies the page/candidate safety cap and
  configured limit when source coverage was cut short.
- `skippedSearches` / `discardedSearches`: never attempted versus attempted but
  not processed; `resumedTargets` identifies work already completed in a checkpoint.
- `allSearchesProcessed`: every planned target was processed, checkpoint-resumed,
  or intentionally skipped because a fill_target run had already met its output
  target.
- `searchPlan`: each target's URL, original/normalized query, market,
  `originalLocation`/`effectiveLocation`, `inputSource` (`query`, `startUrl`, or a
  merged/duplicate value), `attempted`, `executionStatus` (`processed`, `skipped`,
  `discarded`, `resumed`) and reason such as `max_results`, `request_limit`,
  `time_limit`, `circuit_breaker`, `unsupported_language`, `interrupted`, or
  `already_completed`.

Reaching the output cap does not by itself skip later planned targets. A target
that reaches its fair source quota can have `stopReason: max_results` while the
batch remains `sourceStatus: ok`; `fullyCoveredSearches` stays lower than the
completed count because the source was intentionally bounded. Explicit page,
global-candidate, and detail-dependent candidate caps use
`sourceStatus: coverage_limited`, `COVERAGE_LIMIT_REACHED`, and
`truncated: true`; this describes incomplete source coverage, not an Actor
failure. Request/time/challenge or mapping failures remain `partial` or a
stronger failure status. Pure enrichment failures are reported in `detailStatus`
without downgrading an otherwise completed source search.

#### Run summary reference

`RUN_SUMMARY` is available at `/records/RUN_SUMMARY`. Treat it as part of the
result, not as an internal log. A representative successful or partial summary
contains fields such as:

```json
{
  "resultCount": 20,
  "detailsEnriched": 20,
  "detailsFailed": 0,
  "sourceStatus": "ok",
  "limitReached": true,
  "resultLimitReached": true,
  "coverageLimitReached": false,
  "truncated": false,
  "optionalDataStatus": {
    "companyProfile": {"status": "available", "available": 20}
  },
  "requestBudget": {
    "source": {"limit": 50, "consumed": 21, "remaining": 29},
    "detail": {"limit": 20, "consumed": 20, "remaining": 0}
  }
}
```

The example is illustrative: counts and optional groups vary by input. Check
`sourceStatus`, `relevanceStatus`, matched/unscored counts, `searchPlan`,
`resultCount`, `detailsFailed`, `truncated`, stop/coverage fields, and each
requested group's status before reporting success. Optional data can be
missing for a legitimate listing, restricted, skipped by budget, or failed to
load; these cases are not interchangeable.

For a retry, first distinguish `partial`/`timeout`/`request_limit`/`challenge`
from a completed `ok` run. `coverage_limited` means the Actor ran but the source
window was capped; inspect `coverageLimit` before deciding whether to increase
work. Increase the run timeout or relevant process budget
only when the user permits more work; do not treat a second run as a guaranteed
continuation or as a deduplicated global result set. `contractCheck: true` checks
required record shape, not source completeness. An empty completed search does
not itself violate the record contract.

`warning`/`warnings` flag coverage caps, incomplete searches, unconfirmed literal relevance, and
salary review needs. `searches[].filteredBy` counts location, relevance, keywords,
salary, date, classification and custom rejections so a salary rejection is not
misdiagnosed as an unresolved location. `failureStage`/`failureCode` expose bounded
codes such as `rate_limited`, `challenge_detected`, `coverage_limit_reached` and
`contract_failure`, never raw source errors or credentials.

A platform `SUCCEEDED` run can contain `sourceStatus: coverage_limited` with zero
records: the Actor ran successfully, but the configured source window ended
before full coverage. The run log emits status/stage/code and warnings. Genuine
source or validation errors still fail with safe diagnostics. Changing query-validation mode resets
the incremental baseline; the updated scope fingerprint can also reset baselines
from older builds. Concurrent monitoring safety is still not guaranteed.

```bash
apify call bluedata/seek-jobstreet-jobsdb-scraper --input-file examples/basic-search.json --json
apify call bluedata/seek-jobstreet-jobsdb-scraper --input-file examples/company-context.json --json
```

`maxResults` is an accepted-output limit. `maxSourceRequests` and
`maxDetailRequests` are process-only request controls (failed attempts and
retries consume them); none of these inputs is a money or price cap.
Detail fetch concurrency is managed automatically by the Actor and is not a
user-facing input. Review `RUN_SUMMARY` and `sourceStatus` before treating a run
as complete. Requested
optional groups can be `available`, `not_provided`, `restricted`, `failed`, or
`skipped_budget`; these statuses describe observed availability, not a
completeness guarantee.

**Input examples**

**Choose your company enrichment** — a small sample without enabling public
contacts or login-only data. Switch `market` to reuse this workflow elsewhere.

```json
{
  "query": "software engineer",
  "market": "SG",
  "preset": "company",
  "maxResults": 3,
  "additionalData": {
    "companyProfile": true,
    "workContext": true,
    "applicationInfo": true
  }
}
```

Selected detail groups fetch listing details even when `enrichDetails` is false;
that switch separately controls legacy description/apply-link enrichment.
Inspect `optionalDataStatus` and `RUN_SUMMARY` before using the results.

**Recommended Agent-first search** — Singapore Senior Software Engineer jobs with compact output.

```json
{
  "query": "Senior Software Engineer",
  "market": "SG",
  "preset": "basic",
  "maxResults": 25,
  "compact": true,
  "contractCheck": true
}
```

**ADVANCED / EXPERIMENTAL recurring monitor** — reuse the same state store and
key on every run. This is not the recommended quick start: concurrent monitor
state, persistence failures, and resume request budgets are not hardened, and
there is no production safety guarantee.
The named store is opened in Apify cloud storage and created on first use. If
state cannot be loaded or saved, check `incrementalStateStatus` and the
`INCREMENTAL_STATE_UNAVAILABLE` warning; records are not falsely marked `isNew`
when no baseline was loaded. Expiration markers are emitted only from a complete
baseline: a coverage-limited run reports `baselineComplete: false` and
`expiredRecordsReliable: false`, suppresses `emitExpired` output, and adds the
`BASELINE_INCOMPLETE` warning.

```json
{
  "query": "Backend Engineer",
  "market": "SG",
  "maxResults": 50,
  "compact": true,
  "incremental": true,
  "stateStoreName": "my-private-hiring-state",
  "stateKey": "sg-backend-daily",
  "notifyOnlyChanges": true
}
```

**Basic keyword search** — 25 Thailand developer jobs, no frills.

```json
{
  "query": "developer",
  "market": "TH",
  "maxResults": 25
}
```

**Singapore with details** — full descriptions and apply links included.

```json
{
  "query": "software engineer",
  "market": "SG",
  "maxResults": 25,
  "enrichDetails": true,
  "additionalData": {
    "companyProfile": true,
    "companyReviews": true
  }
}
```

**Australia, software-only + incremental** — category filter and
change flags for a scheduled monitor.

```json
{
  "query": "developer",
  "market": "AU",
  "maxResults": 100,
  "categoryIds": "6290",
  "incremental": true,
  "stateStoreName": "my-private-hiring-state",
  "stateKey": "au-developer-monitor"
}
```

### Output

Each run produces a dataset of structured job records, downloadable as JSON,
CSV, or the native Excel export from the Dataset tab in Apify Console, plus a
`RUN_SUMMARY` record with counts, per-search status, optional-data availability,
and source status. **Canonical JSON is the representation for nested optional groups;
it preserves the null/empty-value shape.** Native Dataset export behavior is
unchanged: its JSON, CSV, and native Excel renderings may flatten nested objects
and arrays into separate columns and may omit or reshape null and empty values;
it is not the custom workbook.

#### Optional custom XLSX

Set `exportExcel` to `true` on a stateless run (`incremental` must be false) and
set `maxResults` to 1–1000. After collection returns, the enabled-only output
link downloads the fixed `JOBS.xlsx` record from the default key-value store.
The run summary is at `/records/RUN_SUMMARY`; inspect `excelExport.status`,
`rowCount`, `recordKey`, and the source coverage fields before using the file.
A failed requested export is reported as `failed` and is not advertised as
available. A partial or coverage-limited source run may still provide a workbook
of its acknowledged rows, but its status does not claim complete source coverage.

The custom workbook keeps a readable `Jobs` sheet with field/part rows and a
`Canonical` sheet whose JSON chunks reconstruct the projected Dataset records
exactly, including nested values, nulls, empty values, and large integers. Long
text is split into UTF-16-safe cell parts. XML-invalid controls and lone
surrogates use reversible `\u{HEX}` display encodings, while canonical JSON is
ASCII encoded with underscore escaping to protect literal Excel escape
sequences. These encodings are workbook-specific; native Dataset exports remain
unchanged.

Use `optionalDataStatus` to distinguish `available`, `not_provided`, `restricted`,
`failed`, and `skipped_budget`; these values do not assert complete source coverage.
For operational decisions, also inspect `salaryParseStatus`, re-check
`canonicalUrl`/`applyUrl`, and do not interpret `isExpired` as an official
employer closure.

#### Example job record (illustrative)

```json
{
  "source": "jobsdb",
  "sourceCountry": "TH",
  "market": "TH",
  "jobId": "12345678",
  "title": "Full Stack Developer",
  "company": "Example Employer",
  "canonicalUrl": "https://th.jobsdb.com/job/12345678",
  "location": "Chiang Mai",
  "salaryText": "40,000 – 50,000 THB per month",
  "salaryMin": 40000,
  "salaryMax": 50000,
  "salaryCurrency": "THB",
  "salaryPeriod": "month",
  "salaryParseStatus": "structured",
  "workType": "Full time",
  "workArrangement": "Office",
  "categories": ["Software"],
  "teaser": "Build and maintain web applications.",
  "postedDaysAgo": 2,
  "postedDate": "2026-10-14",
  "postedDateTime": "2026-10-14T00:00:00+00:00",
  "queryTermsMatched": ["developer"],
  "relevanceScore": 1,
  "sourceStatus": "ok",
  "searchQuery": "developer",
  "searchUrl": "https://th.jobsdb.com/th/developer-jobs",
  "fetchedAt": "2026-10-16T00:00:00+00:00"
}
```

`postedAt` is intentionally omitted from this illustrative record: it is the
source's relative display label (for example, `"2 days ago"`), while
`postedDate`/`postedDateTime` are normalized UTC values. Do not parse `postedAt`
as an ISO timestamp. Neither field confirms that the vacancy is still open;
there is usually no reliable employer closing date.

### Limits and short glossary

- `maxResults`: accepted final output cap, default 20, hard ceiling 10,000,
  range 0–10,000. `0` removes the requested output cap; the Actor still applies
  run, page, and candidate safety limits, and a large scan may be required. It
  is not a spending limit. Custom `JOBS.xlsx` requires 1–1,000.
- `coverageMode`: `best_effort` (default) or `fill_target`. `fill_target`
  widens the page and detail-candidate windows and scans until `maxResults` is
  met or a hard run limit is reached, then reports `targetReached`, `shortfall`,
  and a run-level `stopReason`. It is still bounded by the global candidate cap,
  request budgets, and the run time budget; it is not a spending limit. With
  `maxResults: 0`, `fill_target` is echoed but remains bounded like
  `best_effort`; set a positive `maxResults` to enable target filling.
- `maxSourceRequests`: optional process-run budget for search/detail requests,
  range 0–20,000; failed attempts and retries consume it. Omitted preserves the
  legacy behavior.
- `maxDetailRequests`: optional process-run detail-request budget, range 0–1,000;
  explicit 0 disables detail requests. It is separate from output count.
- `maxRetries`: retries after failed source requests, default 2, range 0–10.
- **Detail concurrency**: the number of detail requests the Actor may process in
  parallel. It is automatically managed (currently default 8) and is not a
  user-facing input; it is different from `maxDetailRequests`, which is a total
  request budget.
- **Enrichment**: fetching detail records after search results, such as full
  descriptions, application links, or optional company groups.
- **Canonical JSON**: the lossless nested record representation. Native Dataset
  CSV/Excel may flatten or reshape it; custom `JOBS.xlsx` includes a Canonical
  reconstruction sheet.
- **Partial coverage**: the run stopped before establishing complete source
  coverage. It is not a successful-complete status.

The Actor also enforces safety limits on source scanning and detail candidates;
those limits can produce a truthful partial run and are not price guarantees.

### Pricing and resource use

The current default configuration uses 256 MB memory. Current pay-per-event
pricing is $0.0005 per default-dataset item ($0.50 per 1,000 items) plus
$0.00005 per run-start event. A run that produces no dataset items still incurs
the start event. For example, 20 default-dataset items and one start event cost
$0.01005 USD before any other adjustments or tax. Expiration markers in the
default dataset are items too; pricing is not limited to newly found jobs, and
there is no separate enrichment event charge in the current configuration.
`coverageMode: fill_target` may scan and enrich more candidates, but the billed
events are unchanged: emitted dataset items and the run start; rejected
candidates are not charged.
Memory above 1 GB increases the number of start events. Consult the Actor's current pricing
tab as the authority for configured prices and usage. This example is not a
spend cap; result and request limits do not cap charges.

### How to run it

1. Open this actor in Apify Console.
2. Enter search keywords, pick a market, and set `maxResults`.
3. Turn on `enrichDetails` if you need descriptions and apply links.
4. Click **Start** and wait. The actor reports `coverage_limited` when a
   configured page/candidate cap stops paging, and `partial` for request/time or
   challenge failures.
5. Export the dataset as JSON, CSV, or Excel.

### AI Agent / MCP quick start

The Actor exposes a structured input/output contract that can be called by an
authorized Agent or MCP client when enabled in the account.

#### CLI

Inspect the input contract before calling:

```bash
apify actors info bluedata/seek-jobstreet-jobsdb-scraper --input
```

Call with structured JSON and machine-readable output:

```bash
apify call bluedata/seek-jobstreet-jobsdb-scraper --input '{"query":"Senior Software Engineer","market":"SG","maxResults":25,"compact":true}' --json
```

Install Apify MCP into an authorized local AI client:

```bash
apify mcp install codex
## or: apify mcp install claude-code
## or: apify mcp install cursor
```

For safer Agent selection, expose only the tools/Actors needed for the workflow
with `--tools`. Do not embed a development token in a shared config.

#### Agent calling rules

- Use this Actor for supported public job-search intents; abstain when a request needs login-only data or unsupported markets/fields. Public contact fields are off by default and require explicit user intent.
- Prefer `compact: true` for tool calls and use `enrichDetails: true` only when descriptions/apply links are required.
- Treat `sourceStatus`, coverage/stop reasons, `RUN_SUMMARY`, and partial status as part of the answer; do not claim completeness when a run is partial.
- Treat recurring monitoring as advanced/experimental: concurrent state,
  persistence failures, and resume budgets are not hardened, so there is no
  production safety guarantee. If used, keep the same `stateStoreName`/`stateKey`
  and review every run rather than assuming durable or concurrent safety.

This section documents the CLI/MCP interface only. Availability and service
levels depend on the account configuration and current listing.

### Use cases

- Salary comparisons across APAC markets with structured, currency-tagged pay bands.
- Recruitment sourcing and talent mapping with company, category, and date filters.
- Advanced/experimental new-job monitoring with incremental change flags; no
  production safety guarantee is implied.
- Cross-country hiring-trend analysis on one schema.
- AI-agent and MCP pipelines via compact mode and the auto-generated OpenAPI spec.
- Enrichment of hiring-company context; public contact fields are a separate sensitive opt-in.

### Availability

Pricing is described above; the Actor's current pricing tab is authoritative.
Actual run duration and available data vary with market, query, requested fields,
source availability, and platform conditions. No fixed runtime, completeness,
or recurring delivery is guaranteed.

### FAQ

#### Which markets are supported?

All eight SEEK markets: AU, NZ (SEEK), SG, MY, ID, PH (JobStreet), HK, TH
(JobsDB). One `market` code per run.

#### How many results can I get?

`maxResults` limits accepted output records after final filters. It is not a
network-request or spending cap: enrichment and filters can require processing
more candidates than the final output count. Set a run timeout and review usage.
Runs that cannot complete the requested search report their status in the run
summary; inspect coverage before treating results as complete.

#### Does it support recurring monitoring?

The existing incremental API remains available as an advanced/experimental
feature. Enable `incremental`, reuse the same `stateStoreName` and `stateKey`,
and keep the search scope stable. Concurrent state, persistence failures, and
resume request budgets are not hardened; there is no production safety
guarantee. The first run establishes a baseline and later runs may flag new or
changed jobs, but each run's status and coverage must be reviewed. `contractCheck`
checks required output fields, not the availability of every optional field.

#### Can I use it with the Apify API / MCP?

Yes, when the Actor is enabled for your account. Use the documented input and
output contract and inspect run status before claiming complete coverage.

#### Is it legal to scrape these sites?

This actor extracts publicly available data without login-only access or
credential use. Public contact fields are disabled by default and included only
when explicitly requested; review the source terms and applicable privacy laws
before using them. Descriptions and company text receive best-effort contact
redaction; this does not guarantee anonymization.

### Disclaimer

This actor accesses only publicly available data. You are responsible for how
you use the extracted data and for complying with each site's terms of use
and applicable data-protection law. This actor is an independent tool and is
not affiliated with, endorsed by, or sponsored by SEEK, JobStreet, or JobsDB.
All trademarks are the property of their respective owners.

# Changelog

This Actor's version history is a separate document: https://apify.com/bluedata/seek-jobstreet-jobsdb-scraper/changelog.md

# Actor input Schema

## `query` (type: `string,array,object`):

Optional keyword, comma-separated keywords, JSON string array, native string array, or {terms:\[...]} for API callers. Each term becomes one search in the selected market, merged and deduplicated by job ID. Leave it empty when using startUrls alone; no hidden default keyword is added to URL-only input. If both query and startUrls are empty, the runtime uses the legacy default query "AI Engineer".

## `queryMatchMode` (type: `string`):

source uses source ranking to choose candidates but emits only records with complete target-specific literal evidence. All parsed query terms are forwarded without translation or synonym substitution in every market. Thai queries in TH and Chinese queries in HK with complete coverage and no literal match return no_results. SG Arabic/Korean queries with no literal match on the first result page terminate as unsupported_language; first-page literal matches remain eligible. Other non-Latin queries with complete evidence-free coverage may be unsupported_language. Configured page/candidate caps return coverage_limited; request/time/challenge failures report their own incomplete status. all_terms requires every query token in selected fields; phrase requires one contiguous phrase. Default fields are title/teaser/description. keywordMatch flags select fields. No semantic guarantee.

## `market` (type: `string`):

Which supported public job market to search.

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

Location text, JSON-string array, or native string array (up to 10). Known aliases are market-specific; unknown locations remain unchanged. Bangkok/Bangkok Metropolitan Area resolve to กรุงเทพมหานคร for TH compatibility; this is label matching, not a guarantee of metropolitan geographic coverage. RUN_SUMMARY.searchPlan/searches report originalLocation (the value you sent) and effectiveLocation (the source label actually used).

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

Optional array of HTTPS search/category URL strings or {url} objects on supported hosts (maximum 20). Both item forms are accepted by the API. A URL without its own query text falls back to its search-slug path; its host selects that target's market. When query is empty, URL targets run without an implicit default keyword. Exact duplicate URLs and URL/query collisions are merged and reported in RUN_SUMMARY.searchPlan.inputSource. Detail pages and homepages are rejected.

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

Run-wide output cap across all queries, not a per-query output quota and not a source coverage guarantee. In best_effort multi-target runs, the source candidate budget is shared fairly so each planned target gets an execution opportunity; fill_target may intentionally skip later targets once the run-wide target is met. Final emitted records never exceed this cap. Hard ceiling: 10,000. The Actor scans more candidates than this when location/relevance filters reject listings; resultLimitReached reports the output cap, coverageLimitReached reports a source fetch-window cap; use coverageMode=fill_target with maxResults greater than 0 to keep scanning until this target is met or a run limit is hit. This is not a request or spending cap; enrichment and filtering may process more candidates. 0 removes the requested output cap; exportExcel rejects 0, and a large scan may be required subject to the Actor's safety and time limits. Check run status and usage.

## `coverageMode` (type: `string`):

best_effort (default): bounded scan that stops at the standard safety limits and reports coverage facts in RUN_SUMMARY. fill_target: keeps scanning and enriching candidates until maxResults matched jobs are emitted, the source is exhausted, or a hard run limit is reached. In fill_target runs, RUN_SUMMARY reports targetReached, shortfall, candidatesScanned, candidatesRejected and stopReason, and adds the TARGET_NOT_REACHED warning when the requested target could not be met. fill_target requires maxResults > 0; with maxResults 0 the value is echoed but the run stays bounded like best_effort. Billing is unchanged: emitted dataset items plus the run-start event are charged; rejected candidates are not charged.

## `exportExcel` (type: `boolean`):

Opt in to an Actor-owned JOBS.xlsx download after collection. Requires stateless mode (incremental=false) and maxResults from 1 through 1000; native Dataset exports remain unchanged.

## `preset` (type: `string`):

Choose the enrichment depth. Basic is fastest; Company context enforces workContext, companyProfile, and applicationInfo; Full enforces all supported public groups except contacts and login-only data. Preset groups cannot be disabled with additionalData=false; choose Basic for no enrichment. Detail concurrency is managed automatically.

## `maxRetries` (type: `integer`):

Retries after a failed source request (default 2, range 0-10). Retries consume the configured source request budget.

## `maxSourceRequests` (type: `integer`):

Optional process-run request budget across search and detail requests (range 0-20,000), including failed requests and retries. Omit to preserve legacy behavior; this is not a money cap.

## `maxDetailRequests` (type: `integer`):

Optional process-run detail-request budget (range 0-1,000). Explicit zero disables detail requests; omit to preserve legacy behavior. This is separate from maxResults and automatic detail concurrency.

## `sortMode` (type: `string`):

Result ordering: relevance or newest first.

## `salaryMin` (type: `number`):

Keep jobs whose pay band reaches at least this amount (market home currency).

## `salaryMax` (type: `number`):

Keep jobs whose pay band starts at or below this amount (market home currency).

## `classification` (type: `string`):

Keep jobs whose category labels contain this text. Labels are in the market's local language, so copy an exact value from the 'categories' field of a sample run. For exact category IDs use categoryIds below.

## `categoryIds` (type: `string`):

Optional comma-separated classification IDs (for example, a software category). IDs can be read from an enriched record's classification fields.

## `includeKeywords` (type: `string`):

Comma-separated or JSON-list terms. Every term must appear in the searched fields.

## `excludeKeywords` (type: `string`):

Comma-separated or JSON-list terms. A job is excluded if any term appears in the searched fields.

## `keywordMatchTitle` (type: `boolean`):

Match job titles (combine with other toggles). With no include/exclude keywords, requires all main-query tokens in selected fields. With explicit include/exclude terms, preserves legacy keyword scope; set queryMatchMode=all_terms to validate the main query too.

## `keywordMatchCompany` (type: `boolean`):

Restrict keyword matching to company names.

## `keywordMatchLocation` (type: `boolean`):

Restrict keyword matching to locations.

## `keywordMatchCategory` (type: `boolean`):

Restrict keyword matching to category labels.

## `keywordMatchDescription` (type: `boolean`):

Scope include/exclude keywords to teaser/description text. Main-query strict matching checks actual description only, not teaser presented as a description; enable enrichDetails to fetch it.

## `fromDate` (type: `string`):

Keep jobs posted on or after this date (YYYY-MM-DD or ISO). Records without a date are kept.

## `toDate` (type: `string`):

Keep jobs posted on or before this date (YYYY-MM-DD or ISO). Records without a date are kept.

## `maxAgeMinutes` (type: `integer`):

Keep only jobs posted within this many minutes. Records without a timestamp are kept.

## `customFilters` (type: `array`):

Advanced record-field filters, e.g. \[{"field":"salaryMin","op":"gte","value":"50000"}]. Operators: includes, notIncludes, equals, notEquals, gt, gte, lt, lte.

## `additionalData` (type: `object`):

Select extra public listing and company information. Every group is off by default; availability varies by listing and market.

## `notificationDataGroups` (type: `array`):

Allowlist optional data groups that may be included in webhook notifications. Defaults to none; public contacts require explicit selection here as well.

## `enrichDetails` (type: `boolean`):

Detail enrichment: fetches each accepted listing's detail record for full descriptions, application links, company pages, and classification fields when available. More detail work takes longer; optional groups can trigger detail fetches independently.

## `descriptionMaxLength` (type: `integer`):

Truncate teaser/description text to this many characters (0 = no truncation).

## `compact` (type: `boolean`):

Omit teaser text and matched query terms from each record.

## `excludeEmptyFields` (type: `boolean`):

Remove null and empty optional values from each record.

## `contractCheck` (type: `boolean`):

Validate that returned records carry the expected fields; fail the run when the returned shape is incompatible. Useful for scheduled monitoring.

## `incremental` (type: `boolean`):

Track seen jobs across runs and flag isNew/isUpdated per record. For monitoring, reuse the same persistent stateStoreName and stateKey on every run; without them, a new run cannot load the previous baseline.

## `stateKey` (type: `string`):

Stable key that isolates one monitor's baseline (for example sg-senior-backend-daily). Reuse the same key for that monitor; use a new key when the query or market scope changes.

## `stateStoreName` (type: `string`):

Required for cross-run monitoring: a named cloud key-value store that persists between runs and is created on first use. Reuse the same store and stateKey for the monitor; the default run store does not preserve a baseline. Check RUN_SUMMARY.incrementalStateStatus if storage is unavailable.

## `emitUnchanged` (type: `boolean`):

In incremental mode, also emit records that have not changed.

## `emitExpired` (type: `boolean`):

In incremental mode, also emit markers for previously seen jobs that no longer appear. Markers are emitted only when this run achieved complete source coverage; a coverage-limited baseline sets baselineComplete=false and expiredRecordsReliable=false and suppresses all expiration markers.

## `skipReposts` (type: `boolean`):

In incremental mode, drop new listings whose content matches a previously expired one (flagged isRepost/repostOfId either way).

## `webhookUrl` (type: `string`):

Generic webhook receiving a JSON POST with run metadata and records.

## `webhookHeaders` (type: `object`):

Optional headers for the webhook POST, e.g. {"Authorization": "Bearer xyz"}.

## `discordWebhookUrl` (type: `string`):

Discord channel webhook for run alerts (messages chunked to the 2,000-character limit).

## `notifyOnlyChanges` (type: `boolean`):

With incremental mode, send notifications only when new/updated jobs exist.

## `notificationLimit` (type: `integer`):

Maximum records included per notification (1–20).

## `includeRunSummary` (type: `boolean`):

Include run metadata in notification payloads.

## Actor input object example

```json
{
  "query": "",
  "queryMatchMode": "source",
  "market": "TH",
  "startUrls": [],
  "maxResults": 20,
  "coverageMode": "best_effort",
  "exportExcel": false,
  "preset": "basic",
  "maxRetries": 2,
  "sortMode": "relevance",
  "keywordMatchTitle": false,
  "keywordMatchCompany": false,
  "keywordMatchLocation": false,
  "keywordMatchCategory": false,
  "keywordMatchDescription": false,
  "customFilters": [],
  "additionalData": {},
  "notificationDataGroups": [],
  "enrichDetails": false,
  "compact": false,
  "excludeEmptyFields": false,
  "contractCheck": false,
  "incremental": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "skipReposts": false,
  "notifyOnlyChanges": false,
  "notificationLimit": 5,
  "includeRunSummary": true
}
```

# Actor output Schema

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

Job records with explicit literal relevanceStatus/sourceStatus. Every emitted record has verified query evidence and relevanceScore 1. Inspect matchedTerms, matchedFields, queryLanguage and original/normalized queries. Optional data groups appear only when requested.

## `excelExport` (type: `string`):

Actor-owned JOBS.xlsx with readable Jobs field/part rows and an exact Canonical reconstruction sheet. Available only when the requested export artifact succeeds; it may contain acknowledged rows from a partial or coverage_limited run and is distinct from native Dataset Export XLSX.

## `runSummary` (type: `string`):

Collection and relevance statuses, matched counts, coverage, and searchPlan. processedSearches counts executed targets; completedSearches counts terminal outcomes; fullyCoveredSearches counts exhaustive source scans. resultLimitReached reports that the requested maxResults output cap was met; coverageLimitReached reports that a source page/candidate cap cut the fetch window short. These are independent: a run can hit maxResults with complete coverage, or be coverage_limited without meeting maxResults. coverageLimitedSearches and searches\[].coverageLimit explain configured caps; sourceStatus=coverage_limited is not an Actor failure. coverageComplete is always an explicit boolean on processed and skipped searches. searchPlan and searches expose originalLocation (user input) and effectiveLocation (source-normalized label). detailStatus separately reports enrichment. detailsEnriched counts valid detail responses per candidate record and may exceed resultCount when later filters/caps remove candidates. coverageMode echoes the requested mode; requestedResults, targetReached, shortfall, candidatesScanned, candidatesRejected, run-level stopReason, and resultNotice state how close the run came to the maxResults target and why it stopped. In incremental runs, baselineComplete=false means this run did not observe the full baseline scope; expiration markers are suppressed for incomplete coverage. All parsed queries are forwarded without translation or synonym substitution in every market. Thai in TH and Chinese in HK with complete no-match coverage return no_results; some other language/source combinations can return unsupported_language. Configured page/candidate caps return coverage_limited; request/time/challenge failures report their own incomplete status. When fill_target cannot meet the requested target, the run adds TARGET_NOT_REACHED and a shortfall. Literal matches remain eligible.

# 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 = {
    "query": "",
    "market": "TH"
};

// Run the Actor and wait for it to finish
const run = await client.actor("bluedata/seek-jobstreet-jobsdb-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 = {
    "query": "",
    "market": "TH",
}

# Run the Actor and wait for it to finish
run = client.actor("bluedata/seek-jobstreet-jobsdb-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 '{
  "query": "",
  "market": "TH"
}' |
apify call bluedata/seek-jobstreet-jobsdb-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,bluedata/seek-jobstreet-jobsdb-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/Yl6HhSnan80qaRFT1/builds/xHYAelUUVKWNBb7O3/openapi.json
