# RoleSprig — Job Postings API (`epubpreflight/ats-jobs-api`) Actor

Extract public job postings from Greenhouse and Ashby career pages into consistent JSON for job boards, recruiting workflows, and AI agents.

- **URL**: https://apify.com/epubpreflight/ats-jobs-api.md
- **Developed by:** [Ezra Evercreech](https://apify.com/epubpreflight) (community)
- **Categories:** Jobs, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.75 / 1,000 job 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?

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

## RoleSprig

**Public job listings, ready for your workflow.**

Give RoleSprig one or more supported Greenhouse or Ashby public job-board URLs. It fetches the current postings and returns consistent JSON rows that are ready for job boards, recruiting workflows, automation, and AI agents.

**RoleSprig is an independent tool. It is not affiliated with, endorsed by, or an official product of Greenhouse or Ashby.** Greenhouse and Ashby names are used only to describe the public job-board sources currently supported.

No LLM, model API, browser, proxy, or paid search API is required at runtime.

### Quick start

Supported board roots:

- Greenhouse: `https://boards.greenhouse.io/<board_token>`
- Greenhouse: `https://job-boards.greenhouse.io/<board_token>`
- Ashby: `https://jobs.ashbyhq.com/<job_board_name>`

Only board root URLs are accepted. Individual job URLs, arbitrary hosts, credentials, query strings, fragments, and unexpected redirects are rejected.

Example input:

```json
{
  "board_urls": [
    "https://job-boards.greenhouse.io/greycroft",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "title_keywords": ["engineer"],
  "location_keywords": ["remote"],
  "max_jobs": 100,
  "include_description": false
}
```

`title_keywords` and `location_keywords` use case-insensitive substring matching. Values inside each array are ORed; the title and location groups are ANDed. `max_jobs` is the maximum number of returned rows across the whole run, not per board.

`include_description` defaults to `false`. Enable it only when you need the source description text; descriptions can make the output substantially larger.

### REST API example

Set your Apify token in `APIFY_TOKEN`, then copy and run this request. It uses the Actor's current default build and returns Dataset rows directly:

```bash
curl --fail-with-body --silent --show-error --location \
  --request POST \
  'https://api.apify.com/v2/acts/EpubPreflight~ats-jobs-api/run-sync-get-dataset-items?memory=256&maxTotalChargeUsd=0.00080' \
  --header "Authorization: Bearer ${APIFY_TOKEN}" \
  --header 'Content-Type: application/json' \
  --data '{"board_urls":["https://job-boards.greenhouse.io/greycroft"],"title_keywords":[],"location_keywords":[],"max_jobs":1,"include_description":false}'
```

This exact **versionless** request was verified against the private Actor's current default build on 2026-09-05 and returned exactly one normalized Greenhouse row.

A captured verified response row from the same board and schema was:

```json
[
  {
    "schema_version": 1,
    "source": "greenhouse",
    "board_id": "greycroft",
    "job_id": "4729761005",
    "title": "Executive Assistant",
    "company_name": null,
    "locations": ["San Francisco"],
    "department": null,
    "employment_type": null,
    "workplace_type": null,
    "salary_raw": null,
    "job_url": "https://job-boards.greenhouse.io/greycroft/jobs/4729761005",
    "apply_url": null,
    "source_published_at": null,
    "source_updated_at": "2026-09-02T13:53:31-04:00",
    "fetched_at": "2026-09-05T13:05:59.736394Z",
    "description_text": null
  }
]
```

Job listings change upstream, so IDs, titles, timestamps, and available fields in later runs can differ from this captured example.

### Output

Job rows are written to the default Dataset. Run diagnostics and board-level errors are written to the default Key-value store as `SUMMARY`.

Each job row contains:

`schema_version`, `source`, `board_id`, `job_id`, `title`, `company_name`, `locations`, `department`, `employment_type`, `workplace_type`, `salary_raw`, `job_url`, `apply_url`, `source_published_at`, `source_updated_at`, `fetched_at`, `description_text`.

Unavailable source fields remain `null`; RoleSprig does not guess them. Ashby entries with `isListed: false` are excluded.

### Success, partial results, and failures

RoleSprig distinguishes these cases so automations do not have to treat every zero-row result the same way:

- **Success:** all processed boards succeeded or were normally empty. A filter that matches zero jobs is also a successful run.
- **Partial:** at least one board succeeded and at least one board failed. Successful rows are preserved, the Actor run remains `SUCCEEDED`, and `SUMMARY.status` is `partial`.
- **Failed:** every processed source board failed. `SUMMARY.status` is written as `failed` with per-board errors, then the Actor run itself ends as `FAILED`.
- **No paid-row budget:** if the remaining Pay-per-event budget cannot fund even one result row, RoleSprig returns successfully without starting an ATS request and marks the summary as charge-limit reached.

In Apify Console, open the run's Key-value store and inspect `SUMMARY`. API clients can read the same record from the run's `defaultKeyValueStoreId`:

```bash
curl --silent --show-error \
  --header "Authorization: Bearer ${APIFY_TOKEN}" \
  "https://api.apify.com/v2/key-value-stores/${STORE_ID}/records/SUMMARY"
```

`SUMMARY.external_requests` means **outbound HTTP request attempts started by RoleSprig**. It includes attempts that end in timeout or connection errors; it does not prove that the upstream server received every attempt.

### Retry and safety behavior

- At most two boards are requested concurrently.
- Retryable `429` and selected `5xx` responses use at most two retries.
- A valid `Retry-After` value is understood as either delay-seconds or an HTTP-date.
- A valid `Retry-After` of up to 10 seconds is honored. If the source asks for a longer wait, RoleSprig stops that board with `RATE_LIMITED` or `RETRY_LATER` rather than retrying earlier than requested.
- Invalid or missing `Retry-After` values use bounded exponential backoff.
- Redirects are rejected instead of followed.
- Source responses are hard-capped at 10 MB while streaming.
- Duplicate board inputs and duplicate `(source, board_id, job_id)` rows are removed.

### Pricing and spending limits

The current Pay-per-event configuration is:

- Actor start: **$0.00005 per run**
- Job result: **$0.00075 per returned job** (**$0.75 per 1,000 results**)
- Platform usage: included; no separate usage charge is passed through

The start event is charged when the Actor run starts, so a run can still incur the **$0.00005 start charge even when it returns zero job rows**. The per-result event is charged only for rows successfully emitted to the Dataset.

You can set Apify's `maxTotalChargeUsd` for each run. Before fetching source boards, RoleSprig calculates how many paid result rows the remaining budget can support. If it cannot support even one result row, no ATS request is started. Once a batch of source requests has already started, RoleSprig does not claim that a later output limit can retroactively cancel those in-flight requests.

The current prices are an initial launch hypothesis; they are not a claim of guaranteed developer margin or future pricing.

### Source notes

Greenhouse list responses do not expose every normalized field, so fields such as `company_name`, `source_published_at`, `employment_type`, or a dedicated `apply_url` can be `null`. Greenhouse full content is requested only when `include_description=true`.

Current Ashby public responses expose a source job ID, job/apply URLs, workplace and employment information, published time, descriptions, and optional compensation where the source provides them. RoleSprig does not estimate missing salary or other values.

### FAQ

#### Is RoleSprig an official Greenhouse or Ashby product?

No. RoleSprig is independent and is not affiliated with, endorsed by, or an official product of Greenhouse or Ashby.

#### Does RoleSprig use AI or an LLM to collect jobs?

No. Collection uses public HTTP endpoints and deterministic normalization. The JSON output can be consumed by AI agents, but RoleSprig itself does not call an LLM at runtime.

#### What happens if one board is down?

Other successful boards are still returned. Check `SUMMARY.status=partial` and the per-board error entries to see what failed.

#### Are descriptions always included?

No. `include_description=false` is the default. Turn it on only when you need source description text.

#### Can I limit cost?

Yes. Set Apify's per-run spending limit. RoleSprig checks the affordable result count before source fetching and stops before any ATS request when no paid row can be funded.

# Actor input Schema

## `board_urls` (type: `array`):

Required. Add 1–20 board root URLs. Supported: boards.greenhouse.io/<board>, job-boards.greenhouse.io/<board>, and jobs.ashbyhq.com/<board>. Do not paste individual job URLs.

## `title_keywords` (type: `array`):

Optional. Keep jobs whose title contains any of these values, case-insensitively. Leave empty to keep all titles.

## `location_keywords` (type: `array`):

Optional. Keep jobs whose location contains any of these values, case-insensitively. This filter is combined with the title filter using AND. Leave empty to keep all locations.

## `max_jobs` (type: `integer`):

Maximum number of job rows returned across the entire run, not per board. RoleSprig also respects the run's Apify spending limit before fetching sources.

## `include_description` (type: `boolean`):

Include each source job description as plain text. This increases output size. No AI summary, rewrite, or translation is performed.

## Actor input object example

```json
{
  "board_urls": [
    "https://job-boards.greenhouse.io/greycroft",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "title_keywords": [
    "engineer",
    "developer"
  ],
  "location_keywords": [
    "remote",
    "new york"
  ],
  "max_jobs": 5,
  "include_description": false
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

# 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 = {
    "board_urls": [
        "https://job-boards.greenhouse.io/greycroft",
        "https://jobs.ashbyhq.com/ramp"
    ],
    "max_jobs": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("epubpreflight/ats-jobs-api").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 = {
    "board_urls": [
        "https://job-boards.greenhouse.io/greycroft",
        "https://jobs.ashbyhq.com/ramp",
    ],
    "max_jobs": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("epubpreflight/ats-jobs-api").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 '{
  "board_urls": [
    "https://job-boards.greenhouse.io/greycroft",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "max_jobs": 5
}' |
apify call epubpreflight/ats-jobs-api --silent --output-dataset

```

## MCP server setup

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

```

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/DpZjUnxYQBiOfPlfF/builds/FxSkzYJI8LxC5dcNh/openapi.json
