# ConstructionJobs.com Scraper — Full Board (`piquno/constructionjobs-com-scraper`) Actor

Scrape all 2,800+ jobs on ConstructionJobs.com, not the 714 the paged listing exposes. Salary, benefits, trade category and employer profile links. HTTP-only, no browser.

- **URL**: https://apify.com/piquno/constructionjobs-com-scraper.md
- **Developed by:** [Piquno](https://apify.com/piquno) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 jobs

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## ConstructionJobs.com Scraper — Full Board

Scrape **all 2,800+ live jobs** on ConstructionJobs.com, the main US construction-trades board. Superintendents, estimators, project managers, skilled trades, equipment operators.

HTTP-only, no browser. 28 flat fields per job, ready for CSV or Excel.

**$2.00 per 1,000 jobs.** No charge for empty rows.

***

### Why this one

**Nothing else on the Store covers this site.** Four searches for it returned only unrelated boards.

More importantly, the obvious way to scrape it is broken, and quietly so.

#### The paged listing hides 75% of the board

`/jobs?page=N` clamps at **page 34** and then re-serves that same page forever. Verified: page 35 returned a byte-for-byte identical body to page 34, with all 22 URLs the same.

A page-loop scraper therefore collects roughly **714 of 2,837 jobs**, then spins on duplicates without ever terminating — and reports success the whole time. This actor seeds from the sitemap instead and reaches the entire board. The paged listing is offered only as a deliberate "recent only" mode for cheap daily watches.

#### The site's structured data does not parse

Every job page carries a JSON-LD `JobPosting` block, and `JSON.parse` throws on **100% of them**, for two independent reasons:

- The `description` field holds raw HTML with **unescaped double quotes** (`<div style="margin-left:auto;">`), which terminates the JSON string early.
- `PostalAddress` carries a **trailing comma** before its closing brace.

This actor repairs both and recovers the complete record, description included.

#### Its salary figure is wrong, so we don't use it

The JSON-LD `baseSalary` has been observed as `minValue: 0, maxValue: 0` on postings that display a real range, and elsewhere as `min == max` with the wrong period. Salary is read from the page's own Salary block instead — **scoped to the posting**, because the page also renders a "Related Jobs" rail containing other jobs' pay. A body-wide match silently attaches a different job's salary, and there's a regression test for exactly that.

***

### Input

```json
{
  "seedMode": "sitemap",
  "maxJobs": 500,
  "titleContains": "superintendent",
  "locationState": "Texas"
}
```

**Defaults:** `maxJobs` is **50**, a quick sample that finishes in about 80 seconds. Each job is one request at roughly one per second, so raise it for a full extract and give the run enough time.

| Mode | Reaches | Use for |
|---|---|---|
| **Full board (sitemap)** | all 2,837 jobs | complete extracts, market analysis |
| **Recent only (paged)** | newest ~714 | cheap daily monitoring |

Each job is one request, so `maxJobs` is also your request count.

#### Monitoring the board

Set `incremental: true` with `seedMode: "recent"` and its own `stateKey`. Only postings not seen before are emitted, tagged `changeType: "new"`.

***

### Output

```json
{
  "jobId": "14305",
  "title": "Yard Truck Driver - Yard Jockey",
  "normalizedJobTitle": "CDL Driver, Yard Associate",
  "jobFunction": "Equipment & Fleet, Warehouse & Distribution",
  "employmentType": "FULL_TIME",
  "employerName": "48forty Solutions and Relogistics Services",
  "employerProfileUrl": "https://constructionjobs.com/employer/48forty-solutions-and-relogistics-services",
  "locationCity": "Fontana",
  "locationState": "California",
  "locationPostalCode": "92335",
  "salaryMin": 30,
  "salaryMax": 30,
  "salaryPeriod": "hourly",
  "educationLevel": "Any",
  "experienceLevel": "N/A",
  "vacancies": 1,
  "datePosted": "2026-07-09T17:02:26.000Z",
  "validThrough": "2026-09-21T00:00:00.000Z",
  "fieldsPopulated": 27
}
```

`employerSlug` and `employerProfileUrl` let you join postings to the site's 16,589 employer profiles.

#### Field fill rates

| Field | Fill |
|---|---|
| `jobId`, `title`, `employerName`, `locationCity`, `datePosted` | 100% across all test fixtures |
| `jobFunction`, `educationLevel`, `experienceLevel`, `vacancies` | high |
| `salaryMin` / `salaryMax` | **a minority** — most construction postings withhold pay |
| `description` | 100%, recovered from the malformed JSON-LD |

Nothing is inferred. A posting whose own block reads "Salary Not Specified" returns `null`, not a number borrowed from elsewhere on the page.

***

### Pricing

| Event | Price |
|---|---|
| Job | **$0.002** ($2.00 / 1,000) |
| Actor start | $0.00005 |

A row is billed only if it has a job id or title, and it is billed **before** it is pushed — so you are never charged for a row you did not receive. Rows removed by your filters are never billed.

***

### Limitations — read before you buy

- **One request per job.** There is no bulk endpoint; a full-board extract is ~2,800 requests. Budget the time.
- **Salary is often absent.** Use `withSalaryOnly` if you need it, and expect a much smaller result set.
- **Employer contact details are not available.** Phone and email sit behind a login wall on employer profiles and are simply not in the HTML. This actor does not promise them.
- **`experienceLevel` is frequently "N/A"** — that is what the employer entered, not a parsing failure.
- **Format risk.** If the site fixes its JSON-LD, the repair path falls through to the standard parser automatically. If the structure changes more deeply, the actor logs and skips rather than emitting partial rows.

***

### Running locally

```bash
npm install
npm test
```

Tests run against captured live pages in `test/fixtures/` — no network required. They assert the things that actually break this scraper: that the sitemap yields 2,800+ jobs, that malformed JSON-LD repairs on every fixture, that a trailing-comma fix alone is *not* sufficient, and that salary is never borrowed from the Related Jobs rail.

# Actor input Schema

## `seedMode` (type: `string`):

Full board reads the sitemap and reaches all 2,800+ live jobs. Recent only walks the paged listing, which the site caps at 34 pages (~714 jobs) — faster, but it cannot see the rest of the board.

## `maxJobs` (type: `integer`):

Hard limit. Each job is one request, so this is also your request count — roughly one and a half jobs per second. The default of 50 is a quick sample; raise it for a full extract of the 2,800+ board.

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

Optional. Specific ConstructionJobs.com job URLs to scrape. Overrides the crawl mode.

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

Remember job URLs between runs and emit only postings not seen before. Pair with Recent only for a cheap daily watch.

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

Namespace for incremental state. Use a different key per schedule.

## `titleContains` (type: `string`):

Keep only jobs whose title contains this text, e.g. "superintendent" or "electrician".

## `locationState` (type: `string`):

Keep only jobs in this US state, spelled as the site does, e.g. "California", "Texas".

## `employmentType` (type: `string`):

Keep only jobs of this type, e.g. FULL\_TIME, PART\_TIME, CONTRACTOR.

## `withSalaryOnly` (type: `boolean`):

Most construction postings withhold pay, so expect this to reduce the count substantially.

## `proxyConfiguration` (type: `object`):

Apify Proxy is enabled by default. The site serves plain HTTP fine, but the datacenter pool is more reliable than the bare container egress and is included on every plan.

## Actor input object example

```json
{
  "seedMode": "sitemap",
  "maxJobs": 50,
  "incremental": false,
  "stateKey": "default",
  "withSalaryOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per posting: title, employer, location, salary, trade category, education and experience requirements, dates and the full description.

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

Rows pushed, jobs billed, rows filtered out, jobs skipped as already seen, and fetch failures.

# 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 = {
    "maxJobs": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("piquno/constructionjobs-com-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 = { "maxJobs": 50 }

# Run the Actor and wait for it to finish
run = client.actor("piquno/constructionjobs-com-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 '{
  "maxJobs": 50
}' |
apify call piquno/constructionjobs-com-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,piquno/constructionjobs-com-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/UaO4dAFNvKko13tWv/builds/Rfi1agZzwe0Mrlisp/openapi.json
