Skip to main content
Glama
gzchenhao

OpenHire — Real Job Postings, Ghost Jobs Scored

Search jobs

search_jobs
Read-onlyIdempotent

Search live job postings by skills, title, company, location, salary, seniority and remote scope; get ranked results with ghost scores, posting dates and apply channels.

Instructions

Search the live job index by hard filters; returns ranked JobPosting[].

The server does ONLY a hard filter plus a fixed ranking of match-quality × freshness — precise re-ranking is left to you, the client, which holds the user's context. Every result includes the five protocol fields (verified_at, source, ghost_score, response_sla_days, apply_channel) plus datePosted, days_open, remote_scope, eligible_regions, role_group and ghost_reason.

Four fields are written for the person reading your answer (0.8.30): seniority is the level the title names (intern | junior | senior | staff | lead | manager | director | unknown; a product / project / account manager is an individual contributor, so it is never "manager"; the seniority filter's alias ic means every individual-contributor level (intern, junior, senior, staff, and unknown, because most titles name no level), so for a new graduate pass ["intern", "junior", "unknown"] and for an experienced IC ["senior", "staff", "unknown"]; listing levels leaves unknown out unless you name it. If you do not yet know whether the person leads people and how many years they have, ask before the first search rather than guessing the filter); date_source says whose date datePosted is (employer_ats | employer_self_reported | index_first_seen); dates_in_words is one sentence per language ("雇主发布日期 2026-09-23,在架 35 天;最近核验 2026-10-26…") to show verbatim instead of field names; check_before_apply lists what to confirm before applying, each item from a measured fact (no employer date, open over 90 days, no stated salary, remote scope unclear, unrecognised apply host, employer says closed), with an empty list meaning nothing stood out, not that the posting is vetted. Show at most 5 postings first, as six items each (title; company and location; salary, saying when none is stated; why it matches; what to confirm; apply link), then offer more.

An empty result carries status and a fixed bilingual headline saying which kind of nothing it is: index_empty, known_not_indexed, unknown_company, unknown_tag, unknown_location (no live posting's location matches the place: try the other language, a city, or China / Bay Area) or no_match; filters_applied lists every filter you sent; index_status: "preparing" means the index is still downloading on first start, so say that and retry in a minute rather than reporting a dry market.

verified_at and ghost_score answer DIFFERENT questions and routinely disagree: a posting confirmed live today can score 1.0. Live means the employer's ATS still returns it; the score means it has been returned for a long time, or keeps being relisted. ghost_reason says which input drove the score ("age only: open 367d, never relisted") so you can tell the user that instead of a bare number. Neither field measures intent — a long-open role can equally mean hard-to-fill.

How to say it: ghost_score is a measurement of time on the market, never a verdict that a posting is fake. Do not call a posting a ghost, zombie, fake job or 僵尸岗 on the strength of it, and do not tell the user "don't apply" because of it. Report what was measured — "open 1,616 days, never relisted, the ATS reports no last-touched date" — and let the user weigh it. A 1.0 can be an abandoned req or a role that has been genuinely hard to fill for four years; the number cannot tell those apart.

response_sla_days is null on almost every row, and that is a meaningful null: it is the employer's OWN committed reply window, set only when they claim their tenant. We never infer or estimate it — the application deep-links to the employer and never touches this server, so we cannot observe a reply even in principle. Read null as "no employer has claimed this tenant", never as "missing data" or "slow".

employer_correction appears only when the employer has claimed this tenant and said something about this specific role. It is the employer's own account, verified by corporate identity, and it sits BESIDE ghost_score, never on it: status: "evergreen" explains a high score (they hire continuously, so there is no single opening to fill), it does not lower it. status: "closed" means they say they are no longer hiring even though their ATS still returns the row, so stop sending people there. date_semantics: "requisition_created" means their ATS reports the day the req was opened internally rather than the day it went live, so days_open overstates for that employer. Treat all of it as the employer's claim, attributed, not as our measurement.

days_since_update is the third date and the one that usually settles it: the employer's own last-touched timestamp from their ATS. It means the record was saved, not that the content changed or that someone is reviewing applicants: say "the employer's system shows it was last updated N days ago", never "someone is actively managing it". Two rows can both score 1.0 and mean opposite things — open 367d and untouched for 367d reads as abandoned; open 327d but touched 13 days ago reads as a tended evergreen req. Among rows where the ATS reports one at all, the share of ghost>=0.99 postings touched by the employer inside 30 days has ranged from about a third to three quarters across refreshes; read the current value from pct_ghost_hi_touched_within_30d in docs/numbers.json rather than quoting a figure.

Null is NOT "abandoned": Ashby, Lever and Beisen do not report a last-touched date, and those rows carry update_signal: "not_reported_by_ats" instead. A row carrying date_signal: "not_reported_by_ats" goes one step further: its source reports no posting date at all (Li Auto's first-party mirror is one), so its datePosted and days_open are counted from the day this index first saw it, not from the employer's own date. Read those as a lower bound on age, never as the employer's timeline. Treating that null as "nobody has touched this in a year" would describe the vendor's API, not the employer. Honest limit even when present: an ATS bumps it on any edit or re-publish, so it means "touched", not necessarily "content changed".

To answer "what is hiring?", pass company — do not filter client-side.

Skills are alias-aware on the request side: 感知 finds rows tagged perception, 占用网络 finds occ / occupancy / occupancy networks. If a requested tag matches NOTHING in the index, the response switches to {results, unknown_skills, suggestions} so you can tell the user which word was dropped instead of presenting a half-match as a match.

Live means the employer's own system returned the posting within the last 30 days of this index (counted back from its newest verification); older rows are not returned. An employer whose rows are all older answers with status "not_verified_recently".

role_family filters exclude rows KNOWN to be another family; rows not yet classified (role_family null, typically the newest postings) are included, not hidden. remote_scope can also be "unknown": remote with a location we could not read; it is never reported as "worldwide" any more.

Two things worth knowing before you spend your budget:

  • role_group is shared by the same role posted in several cities — one employer may list one job 22 times, once per location. Those rows are genuinely distinct (each has its own job_id and apply_channel, which matters when the user has a location or visa constraint), but if you only need distinct opportunities, group by role_group and keep one per group. Measured, about 20% of a page is same-role repeats.

  • offset pages through the ranked list. After collapsing by role_group, call again with offset += limit to get more distinct roles. Fewer rows than limit means you reached the end. There is no server-side cursor to keep alive.

  • One call returns at most 100 rows. Ask for more and you get an object with results and truncated, not a silent first page: 100 of 198 looked exactly like "this employer has 100 jobs". truncated: true means a full page came back and there may be more, with the offset to call next in hint; truncated: false means fewer rows than the page size matched and there is no next page. For one employer, get_company_info's active_jobs is the true total.

  • Skill tags match separator-insensitively: "computer vision", "computer-vision" and "computer_vision" are one skill. The extractor emits all three spellings, so an exact-string search reached as little as 42% of the rows that had the skill.

  • An empty search does NOT return a bare list. It returns an object with results: [] plus hint, unknown_skills and suggestions, because [] alone cannot tell you whether you mistyped a tag or the market is genuinely dry. Read unknown_skills: if it is non-empty those tags exist nowhere in the index and you should retry with a suggestion; if it is empty your tags were fine and you should loosen a filter.

Args: skills: skill tags, ANY-overlap match (union), e.g. ["rust", "k8s"]. required_skills: skills that must ALL be present (AND), e.g. ["rust"]. remote: if true, only fully-remote roles. remote_scope: filter remote roles by reach: "worldwide" | "unknown" | "region_locked" | "country_locked". min_salary: salary floor. By default roles with NO stated pay are KEPT (they can't be ruled out); set require_stated_salary=true to drop them. currency: restrict to a stated-pay currency, e.g. "USD" (implies stated pay). require_stated_salary: if true, drop roles that publish no salary. role_family: coarse family filter, one of engineering | data | product | design | marketing | sales | hr | ops | other. "hr" is recruiting, talent acquisition, sourcing, HRBP, people operations, compensation and benefits, L&D (招聘、HRBP、 人力资源); "recruiting", "people" and "招聘" resolve to it. Any other value is refused (ERR_UNKNOWN_ROLE_FAMILY) rather than ignored: an unknown value used to pass through and return the whole index. Populated for most live rows, so this is an effective way to keep sales / solutions-architect roles out of an engineering search. title: caseless substrings over the job title, ANY-of, e.g. ["recruit", "招聘"] or ["感知"]. This is the filter for roles no skill tag or family can isolate: finance, legal, a specific team name; for HR it is a wider net than role_family="hr". An ASCII term must start a word ("hr" reaches HR, HRBP and HR Business Partner but not Chrome); a CJK term is a plain substring. One synonym group is expanded for you: any of hr / hrbp / human resources / recruit / talent acquisition / sourcer / people ops / 人力 / 人事 / 招聘 reaches all the others, so title=["招聘"] also finds an English "Senior Technical Recruiter". Combine with company to ask "does have any HR openings?" instead of paging their whole list. seniority: keep only rows whose title names one of these levels, ANY-of: intern, junior, senior, staff, lead, manager, director, unknown; plus the aliases "ic" (intern, junior, senior, staff, unknown: everyone who leads no team, including titles that name no level, which cannot be ruled out) and "management" (lead, manager, director). Ask the user once whether they want an individual-contributor or a management role and roughly how many years they have, then pass it: an engineer searching Zoox for perception otherwise gets "Senior Manager, Perception Data" first because it is the newest. A hard filter, never a ranking input; an unknown value is refused (ERR_UNKNOWN_SENIORITY). sector: only employers in one sector: autonomous_driving (自动驾驶 / 智驾, including the lidar, chip and truck companies built for it), embodied_ai (具身 / 机器人), ai_infra (大模型 / AI labs, chips, AI tools) or other. Use it when the user says "only robotics companies" instead of naming employers one by one. Every row carries its sector; a board the user added themselves has none and is left out. Unknown words are refused (ERR_UNKNOWN_SECTOR). entry_level: true keeps only postings open to a new graduate (应届生 / new grad): the text says campus hiring, internship or no experience required, or the title names an intern or junior level. Postings that say nothing either way are left out. Every row carries requirements {min_degree, min_years, hire_track, entry_level}, read from the posting's own words with free rules: min_degree is the lowest degree it REQUIRES (a degree it only prefers does not count), and null means the posting did not say, never "not required". Quote it as what the posting says, not as a rule the employer will enforce. posted_within_days: only postings the employer published in the last N days (1 to 365), counted from the employer's own posting date, or from the day this index first saw a row whose system gives none. Use it when the user wants fresh postings only ("只看两周内发的"); it is a filter, the ranking is unchanged. collapse_role_group: keep one row per role_group instead of one per city, and add role_group_size saying how many postings that row stands for. Cheaper when the user wants distinct opportunities; leave it false when location or visa matters, because each city row has its own job_id and apply_channel. company: restrict to one employer. Pass whatever the user said — an id ("unitree"), or any part of the name in either language ("宇树", "Unitree", "XPeng"). Exact id/name hits win; otherwise it is a caseless substring, so a broad word can match several employers. A name this index does not carry comes back as the empty-result object with unknown_companies and suggestions. Some employers are known but deliberately NOT indexed: their careers sites run on Feishu Recruitment, whose job-list API requires a request signature; we treat that as access control and do not work around it. For those (Momenta, 小马智行 Pony.ai, 智元 AgiBot, MiniMax, 智谱 Zhipu, 商汤 SenseTime, 逐际动力 LimX, 自变量 X Square, 千寻智能 Spirit AI, 加速进化 Booster, 穹彻智能 Noematrix, 众擎 EngineAI) the empty-result object carries known_not_indexed with the employer's own careers portal URL, the reason, and employer_opt_in (the employer can authorize the read-only Feishu open-platform scopes hire:site:readonly and hire:site_job_post:readonly). 蔚来 NIO is on the same list for a different reason: its own careers page publishes the postings, but its edge security policy blocks this crawler (HTTP 567) and we do not work around security controls; the employer can allowlist the crawler or authorize the same read-only scopes. Send the user to that portal; do not retry with a looser filter, and do not present the absence as "not hiring". A few more are on the same list because their own site has no readable job list (云深处 DeepRobotics publishes only on BOSS直聘; 智平方, 轻舟智航 QCraft, 鉴智 PhiGent and 松延动力 Noetix keep a page with no list, a form, a JavaScript shell or a robots-disallowed API), and 毫末智行 HAOMO has ceased operations: for it, tell the user that and send them nowhere. location: caseless substring over the employer's location text, either language ("北京", "Beijing", "Mountain View", "Remote"). Alias-aware for the cities Chinese employers spell several ways: 广州 / Guangzhou also reaches rows that name only a district ("广东·天河区", 番禺区, 黄埔区, 南沙区, 海珠区, 越秀区, 白云区), 深圳 / Shenzhen reaches 南山区, 福田区, 龙岗区, 宝安区, Beijing reaches 北京市, and "remote" reaches 远程; 上海, 杭州, 苏州, 南京, 武汉, 成都 and 合肥 match their pinyin too. Combine with remote_scope to keep or drop a country. No location filter means all locations. limit: max results (default 20); must be >= 1 (else ERR_BAD_PAGE). A negative offset clamps to 0.

Salary fields are null wherever the employer's ATS publishes no range, which is most postings outside US states with pay-transparency law; require_stated_salary and currency therefore narrow mostly to US rows, and min_salary alone KEEPS unstated rows (it cannot rule them out). Pass currency to compare in one currency; without it, stated pay is compared as an annualised number in whatever currency it was stated.

Input Schema

NameRequiredDescriptionDefault
limitNo
titleNo
offsetNo
remoteNo
sectorNo
skillsNo
companyNo
currencyNo
locationNo
seniorityNo
min_salaryNo
entry_levelNo
role_familyNo
remote_scopeNo
required_skillsNo
posted_within_daysNo
collapse_role_groupNo
require_stated_salaryNo

Output Schema

NameRequiredDescriptionDefault
resultYes

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed3 schema fields changedv0.8.41
    • addedInput schema / properties / entry_level
      Added value: +{
      +  "default": false,
      +  "title": "Entry Level",
      +  "type": "boolean"
      +}
    • addedInput schema / properties / posted_within_days
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "integer"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "title": "Posted Within Days"
      +}
    • addedInput schema / properties / sector
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "title": "Sector"
      +}
  2. Changed1 schema field changedv0.8.31
    • addedInput schema / properties / seniority
      Added value: +{
      +  "anyOf": [
      +    {
      +      "items": {
      +        "type": "string"
      +      },
      +      "type": "array"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "title": "Seniority"
      +}
  3. Changed1 schema field changedv0.6.8
    • addedInput schema / properties / title
      Added value: +{
      +  "anyOf": [
      +    {
      +      "items": {
      +        "type": "string"
      +      },
      +      "type": "array"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "title": "Title"
      +}
  4. Changed1 schema field changedv0.6.7
    • addedInput schema / properties / location
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "title": "Location"
      +}
  5. Changed2 schema fields changedv0.6.3
    • addedInput schema / properties / collapse_role_group
      Added value: +{
      +  "default": false,
      +  "title": "Collapse Role Group",
      +  "type": "boolean"
      +}
    • addedInput schema / properties / company
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "string"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "title": "Company"
      +}
  6. Changed1 schema field changedv0.5.0
    • addedInput schema / properties / offset
      Added value: +{
      +  "default": 0,
      +  "title": "Offset",
      +  "type": "integer"
      +}
  7. First observedv0.3.2

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare read-only/idempotent/non-destructive, and the description goes well beyond them: the 30-day verification window defining "live", the 100-row cap, index_status:"preparing" on first start, `not_verified_recently` for stale employers, which employers are deliberately not indexed and why (Feishu signature, HTTP 567), and the explicit reinterpretation rules for null response_sla_days and update_signal/date_signal "not_reported_by_ats". It also warns against misuse (never present absence as "not hiring").

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose and paging rules are front-loaded, but the text runs to many hundreds of words with substantial duplication: the empty-result object is described twice, ghost_score semantics are re-litigated across three separate paragraphs, and role_group is explained both in the bullet list and again under collapse_role_group. Large stretches are answer-writing coaching ("Show at most 5 postings first, as six items each") rather than tool-selection content, which dilutes the parts an agent must not miss.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 18-parameter, zero-schema-coverage search tool with siblings, nothing needed for correct invocation is missing: filter interactions, ranking behaviour, truncation, empty-result discrimination, and the known_not_indexed landscape are all covered. Return-field detail arguably duplicates the output schema, but that is surplus rather than a gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% across 18 params, so the description carries the full burden and does: enum vocabularies and refusal behaviour for role_family/seniority/sector, the ic/management aliases, title's ASCII word-start vs CJK substring rule and synonym expansion, location's district/pinyin aliasing, min_salary keeping unstated rows unless require_stated_salary, and offset/limit clamping with ERR_BAD_PAGE.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence gives a specific verb and resource ("Search the live job index by hard filters") and states the return shape (ranked JobPosting[]). It also delineates the tool's division of labour versus the client ("server does ONLY a hard filter plus a fixed ranking … precise re-ranking is left to you") and points at the sibling get_company_info for true per-employer totals, so an agent can place it among the 12 siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing rules: "To answer 'what is <employer> hiring?', pass company — do not filter client-side", combine title with company to answer targeted questions, use posted_within_days for freshness, collapse_role_group only when location/visa does not matter. Failure paths are spelled out too — offset paging, truncated:true vs false, unknown_skills retry with a suggestion, and not retrying with looser filters for known_not_indexed employers.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.