openroles

io.github.aranculleton由 GitHub 账号 aranculleton 发布v0.4.0更新于 Oct 29, 2026

Open roles from employers' own job boards: Greenhouse, Lever, Ashby, Workday and 12 more.

概览

AI 生成的概览

直接从雇主自有的招聘系统(ATS)职位板读取在招职位,让助手找到某公司的职位板并列出当前空缺。

可以做什么
  • 识别雇主使用哪套招聘系统(Greenhouse、Lever、Ashby、Workday 等共 15 套),可输入公司名、招聘页面或职位板网址
  • 按最新优先列出雇主的在招职位,含职位名称、地点、是否远程、发布日期、部门、公布的薪资和申请链接
  • 按职位名称关键词、地点和远程条件筛选,仅在需要时获取职位描述
适用场景
适合需要从雇主官方来源获取完整、最新职位信息而非招聘聚合站的场景,例如查看某公司的空缺、按地点或职位筛选,或收集申请链接。
运行要求
以 stdio 方式在本机运行,通过 uvx(或 pip)从 PyPI 安装,需要 Python 3.9 或更高版本。无需 API 密钥、账号或环境变量;它读取公开的职位板数据源,需要能访问这些职位板主机的网络。
安装前请注意
只读:它抓取职位板,不会投递、提交或修改任何内容。请求会发往第三方雇主主机,对方可见这些流量;它会遵守 robots.txt 与 AI 输入相关声明,部分系统被有意排除。自 2027 年 2 月起 Recruitee 职位板需要各雇主自己的令牌。

安装

在 SourceWeft 中

  1. 打开 控制台中的 openroles,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

openroles

Every open role, straight from the employer's own careers system. Fifteen applicant tracking systems (Greenhouse, Lever, Ashby, Workday, Workable, Teamtailor, Personio, Recruitee, Rippling, JazzHR, Gem, Polymer, Manatal, Homerun and ClearCompany), plus any careers site that publishes its jobs for search engines, in one shape. Read from the feeds and APIs the employers' own careers pages use: no API key, no account, no scraping service, and nothing but the Python standard library.

[CI] [PyPI] [Python] [License: MIT]

console
$ pip install openroles$ openroles figma --query data --limit 32026-09-23  Data Engineer Intern (2027)      San Francisco, CA • New York, NY          https://boards.greenhouse.io/figma/jobs/6178851004?gh_jid=61788510042026-09-21  Data Platform Engineer           San Francisco, CA • New York, NY • Unite  https://boards.greenhouse.io/figma/jobs/6201407004?gh_jid=62014070042026-09-21  PhD Intern, Data Science (2027)  San Francisco, CA • New York, NY          https://boards.greenhouse.io/figma/jobs/6200626004?gh_jid=6200626004
3 open role(s)

Give it a company name, a careers page, a board URL or ats:token. It finds the board, reads every open role, and gives you the same fields whichever system the employer uses.

Why

Job sites miss roles, show them late, and repost them stale. The employer's own board is the source: it is complete, current, and carries the real apply link. Most applicant tracking systems publish that board as JSON, RSS or XML for the employer's own careers page. openroles reads it the same way, politely, and puts every system that allows it behind one function.

Install

bash
pip install openroles          # or: uvx openroles <company>

Python 3.9 or newer. No dependencies.

Command line

bash
openroles linear                                   # every open role at Linearopenroles https://jobs.ashbyhq.com/linear          # the same, from the board URLopenroles lever:zoox --query "data" --since 14     # title words, posted in the last 14 daysopenroles figma --location "New York"              # filter by place (--remote for remote only)openroles https://adobe.wd5.myworkdayjobs.com/external_experienced \          --query "data scientist" --details       # Workday, with each posting's descriptionopenroles detect "Figma"                           # which ATS, and the board's tokenopenroles hosts greenhouse:figma lever:zoox         # the hosts to allow in a network policyopenroles personio:acme                            # any system, as ats:tokenopenroles careersite:careers.acme.example          # a careers site with no job APIopenroles linear --json > roles.json               # JSON (the default when piped), or --jsonlopenroles linear --cache ~/.cache/openroles        # an unchanged board sends nothing next run

Python

python
import openroles
openroles.detect("Figma")# [Board(ats='greenhouse', token='figma'), Board(ats='ashby', token='figma')]
jobs = openroles.fetch("greenhouse:figma", query="data", location="New York")for job in jobs:    print(job.posted, job.title, job.location, job.url)
job.to_dict()   # plain JSON-ready dict
openroles.hosts("personio:acme")# ['acme.jobs.personio.de', 'acme.jobs.personio.com']: what a firewall must allow
openroles.job_id("https://www.acme.example/careers/?gh_jid=4012345")# 'gh-4012345': the same id fetch() gives that posting, from its address alone

job_id is for matching one posting found in two places, read from the employer's board and linked from a job site. It reads Greenhouse (any page with gh_jid too), Lever, Ashby, Rippling, Workable, Personio and Workday addresses, fetches nothing, and gives "" for an address that does not carry the system's own posting id. One Workday case is ambiguous: a listing that sends its path already percent-encoded ("M%C3%BCnchen") gives the same address as one that sends it plain ("München"), and job_id reads it as the plain one.

fetch() raises openroles.BoardError when a board cannot be read (a wrong token, no answer), and openroles.RobotsDisallowed when the host's robots.txt says no.

In your own application

openroles is a library first: the CLI and the MCP server are thin layers over fetch and detect. An application that embeds it names itself once, at start-up:

python
openroles.configure(    user_agent="acme-jobs/1.0 (+https://acme.example/contact)",  # sent with every request    robots_agents=["acme-jobs"],   # a Disallow for your agent stops a read too    cache_dir="~/.cache/acme-jobs/openroles",   # optional: keep answers between runs)

With cache_dir, each board's last answer is kept with the ETag or Last-Modified date the board sent, and the next run asks with them: a board that honours them and has not changed answers 304 Not Modified and sends nothing, and the kept copy is read instead. An answer that came with a Last-Modified date and no ETag is asked about that way for at most a day (MODIFIED_MAX_AGE in openroles._http, 24 hours) after it was last read in full, and then read in full once, so a host whose date does not move when its data does holds a board back a day at most; the same holds within one run, with or without a folder. Every read still asks the board. It is off by default. An answer is kept only when it is a GET, is not marked no-store, and shows that all of it arrived: all of its Content-Length, every chunk of a chunked answer, or a gzip stream to its end. One that gives no length and is neither chunked nor gzipped ends when the connection closes, cut short or not, so it is read in full every time. Kept answers are in owner-only files, for 30 days unused, and about 500 MB of them: each process keeps the folder to that, so two processes writing to one folder at once (a long-running application and a command-line run, say) can take it past that until one of them next looks at the whole folder: when it starts using it, at least once an hour while it does, and whenever what it has written itself passes the cap.

Everything a search decides for itself stays a parameter: query, location, remote, since_days, country, and details, which can be a function that says which postings are worth a request for their description. report={} comes back filled with what failed on each board. openroles.robots_allows(host, paths) answers the same robots.txt question openroles asks itself, for anything else your application fetches.

For AI agents (MCP)

openroles mcp is a Model Context Protocol server over stdio, so Claude, Cursor and other agents can look up any employer's open roles themselves. Two tools: find_job_board and list_open_roles (descriptions left out unless asked for, to keep answers short).

Claude Code:

bash
claude mcp add openroles -- uvx openroles mcp

Claude Desktop, Cursor and other clients (mcpServers in their config):

json
{  "mcpServers": {    "openroles": { "command": "uvx", "args": ["openroles", "mcp"] }  }}

There is also llms.txt, a short brief for a model that has to use the library or the CLI.

What you get

Every role, from every system, has the same fields:

Field
idstable across runs: a prefix for the system (gh-, lv-, ab-, wd-, ...) plus its own id
ats, boardwhere it came from, e.g. greenhouse, figma
title, urlthe role and its apply page
location, locationsevery location the posting lists, joined and as a list
workplaceremote, hybrid, onsite, or "" when the board does not say
remoteTrue, False, or None when the board does not say (hybrid is None)
postedYYYY-MM-DD, first published (not last edited) where the system says; see below
department, employment_typewhere the board provides them
compensationthe posted pay band, where the employer publishes one, as the board words it
salary_min, salary_max, salary_currency, salary_periodthe same band as numbers, its ISO currency code and hour, day, week, month, year or "" (a period such as every two weeks), only where the board publishes them as numbers (Lever, Ashby, Recruitee, Rippling, ClearCompany, careers sites); never read out of a description
deadlinewhere the board provides one
descriptionplain text: HTML removed, and with it what no page shows (comments, scripts, hidden elements)

Supported systems

SystemRequestsNotes
Greenhouse1 per boarddescriptions; every office a posting lists counts as one of its locations
Lever1 per boarddescriptions with their requirement lists; EU boards found automatically
Ashby1 per boarddescriptions and posted pay. Ashby publishes no first-posted date, so posted is when the posting was last published
Workday1 per 20 roles, plus 1 per description--query searches the board, reading on while titles keep matching; --country narrows it by whatever facet the board files countries under; --details fetches descriptions, only for roles whose title passes --query
Workable1 per boarddescriptions; no pay. Built from Workable's documentation and not yet run against a live board (its hosts were rate-limiting when it was written)
Teamtailor1 per 100 rolesdescriptions; hybrid and remote; a careers site on the employer's own domain works too
Personio1 per boarddescriptions in English where the employer wrote them; .de and .com accounts
Recruitee1 per boarddescriptions, pay and closing dates. From 10 February 2027 Recruitee asks for each employer's own token, after which a board reads as needing it
Rippling1 per board, plus 1 per descriptiondescriptions and dates come from each posting's own record, fetched only with --details or a --query, at most 80 a board (Rippling allows 100 requests in 10 minutes)
JazzHR1 per boarddescriptions; no date or pay. The feed is regenerated once a day
Gem1 per boarddescriptions; hybrid and remote
Polymer1 per 20 roles, plus 1 per descriptionpay; descriptions as for Rippling, at most 60 a board
Manatal1 per 100 rolesdescriptions; no date
Homerun1 per boarddescriptions and pay, from the feed every Homerun careers page links to
ClearCompany1 per boarddescriptions and pay; the token is the site id the careers page prints. Only its first page is read, with a warning when there is more
Any careers site (careersite)1 per sitemap, plus 1 per job pagereads the schema.org JobPosting each job page carries for search engines (iCIMS and Eightfold sites, among others), at most 40 pages a call, newest first

A Workday posting listed as "3 Locations" reads as its primary location "and 2 more"; with --location on a place that is not its primary, its page is read to see whether the place is one of the others. Workday pages a board only to its first 2,000 roles: past that, a warning says to search with --query.

detect finds a board from a company name (every system but Workday and ClearCompany, by trying the names an employer is likely to use), from any board URL, and from a careers page that links to or embeds one, however the page writes it. A Workday board cannot be guessed from a name: give its URL (either tenant.wdN.myworkdayjobs.com or wdN.myworkdaysite.com/recruiting/tenant). A careers site is never guessed: name it as careersite:<address>. When a careers page uses a system openroles cannot read, a warning names it.

Not supported, and why

Each was checked before it was left out. A system moves onto the list above when it publishes an open board its own terms and robots.txt allow.

SystemWhy not
SmartRecruitersits robots.txt closes its public API to every crawler except LinkedIn's
SuccessFactors, Dayforce, UKG, Paycom, Pinpointrobots.txt disallows the job pages, or the board needs a sign-in
BambooHR, Breezytheir terms forbid automated reading of the careers pages
Cornerstone, BrassRing, Hireology, Comeetthe board is read through a session token issued to the browser
Phenomthe sites checked ask, through robots.txt's Content-Signal, that their content not be used as AI input
Oracle Recruitingits job API is marked for internal use, and the job pages build their JobPosting in the browser
Freshteambeing shut down by its vendor
iCIMS, Eightfoldno open job API, but their careers sites usually publish JobPosting pages: read them as careersite:
Taleo, ADP, Paylocity, JOIN, TalentLyft, ApplicantPronot yet checked well enough to build a reader on

Polite by design

  • robots.txt is read first, and a refusal is honoured, never worked around. Where the vendor documents its endpoint as a public API (Greenhouse, Lever, Workable, Teamtailor, Personio, Recruitee, Rippling, JazzHR, Gem, Polymer, Manatal), an explicit Disallow stops a read and a robots.txt that cannot be read does not. Everywhere else (Ashby, Workday, Homerun, ClearCompany, any careers site) robots.txt must be readable and must allow the read. For Workday the careers site's own rules count too.
  • A site that asks not to be AI input is not read. robots.txt's Content-Signal: ai-input=no is honoured, because openroles is often run by an AI agent.
  • A redirect is followed only where its new host's robots.txt allows it, and never more than five times.
  • A host's Crawl-delay is kept between requests once its robots.txt is read (Lever asks for one second), and a robots.txt is read again after a day, as RFC 9309 asks.
  • At most two requests in flight per host, a timeout on every call, gzip on the wire, conditional requests (ETag and Last-Modified) for anything read twice in a run, and across runs with a cache folder, and an honest User-Agent naming this project.
  • XML that declares a DTD or entities is never parsed, so a hostile feed cannot expand.
  • A board that answers 429 or 503 is left alone for the rest of the run, and a documented rate limit (Rippling's 100 requests in 10 minutes) is kept below.

Is this allowed?

Most of these systems document their endpoints as public, for exactly this purpose: showing an employer's open roles somewhere else. Each reader's source file quotes the vendor's own words and says where they are. Workday's, Ashby's, Homerun's and ClearCompany's are the endpoints their own careers pages call; they are not documented, they can change without notice, and openroles reads them only where robots.txt allows. A careers site is read only from the job pages it publishes for search engines, at the pace its robots.txt asks. Use it the way the boards are meant to be used: to find roles and apply to them, at a human pace. It is not a tool for hammering a site or republishing someone's board wholesale.

Where it came from

openroles is the employer-board reader from last-job-search, a job search that Claude runs end to end, extracted so anyone can use it.

Contributing

Contributions are welcome, and the most useful one is a reader for another system, or a check that moves one off the "Not supported" list. CONTRIBUTING.md has the set-up, the few rules that keep the library small and polite, and a checklist for adding a system. Bugs and requests go in Issues.

bash
pip install -e . pytest ruffpytest && ruff check .        # offline: every answer comes from tests/fixtures/

Licence

MIT. See LICENSE.

来源:README.md,提交 3edb660

工具

2

find_job_board

Find which applicant tracking system (Greenhouse, Lever, Ashby, Workday, Workable, Teamtailor, Personio, Recruitee, Rippling, JazzHR, Gem, Polymer, Manatal, Homerun or ClearCompany) an employer posts its jobs on. Give a company name, its careers page URL or a job board URL. Returns boards as ats:token, which list_open_roles accepts.

输入参数target必填

list_open_roles

List an employer's open roles straight from its own job board, newest first: title, location, remote, posted date, department, pay where posted, and the URL to apply. Filter by title words, location and remote. Descriptions are left out unless asked for, to keep the answer short.

输入参数limitquerytarget必填countrylocationsince_daysremote_onlyinclude_descriptions

版本历史

5
  1. v0.4.0最新Oct 26, 2026
  2. v0.3.0Oct 21, 2026
  3. v0.2.0Oct 19, 2026
  4. v0.1.2Oct 17, 2026
  5. v0.1.1Oct 16, 2026

评分

已满足适用检查项的 90%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分
  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分