The Rival Job-Posting Field Reference: Live Seat, Evergreen, or Ghost
Label any rival posting Live Seat, Evergreen, Duplicate, Stale, or Ghost from its public fields, with the rows that justify it, so only real seats enter hiring counts.
Key takeaways
- Only postings labelled Live Seat belong in a competitor hiring-velocity count; Evergreen, Duplicate, Stale, and Ghost postings each overstate investment in a different way.
- Greenhouse's updated_at changes whenever a recruiter edits a job, so a typo fix can make a 200-day-old seat look new; age should come from first_published or your own first-seen snapshot.
- In one King County sample, 88,307 total postings collapsed to 35,662 unique postings, a 2.48x fan-out, and the ratio reached 5.22x for physician assistants, so no single dedup multiplier is safe.
- A blanket 90-day staleness cutoff misfires because Ashby's median time to first fill is 56 days for business roles and 76 days for technical roles; thresholds must be set per role family.
- Greenhouse's 18 to 22% quarterly ghost-job share counts postings that produced no hire, including frozen requisitions, on its own customers only, so it cannot be applied to a single competitor.
- An 11-day median posting duration against 39 to 76 day fill medians means a takedown date alone never proves a seat was filled.
This is the lookup I keep open before a competitor's open-role count goes into a strategy brief. It is for talent-intelligence analysts, strategy teams, and operators who track competitor job postings to read where a rival is investing. It defines what each public posting marker proves and how it misleads, then gives a procedure for labelling every posting Live Seat, Evergreen, Duplicate, Stale, or Ghost with the evidence attached, so that only Live Seats feed hiring-velocity reads.
What the five posting labels mean
Every competitor posting gets exactly one of five labels, and only Live Seat counts toward hiring velocity. The other four explain why a visible ad is not a seat being filled now.
| Label | What it is | Counts toward velocity? |
|---|---|---|
| Live Seat | One seat-group, first seen inside its role-family fill window, no refresh flags | Yes |
| Evergreen | A pipeline ad kept open or reposted past the fill window | No, track as standing demand |
| Duplicate | An extra post pointing at a seat-group already counted | No, adds to fan-out |
| Stale | Plausibly real once, now past threshold and still up | No, re-check for takedown |
| Ghost | Two or more markers that no seat is imminently available | No |
Some terms used throughout:
- Seat-group: all the postings that point to one underlying job, however many locations or boards it appears on.
- Fan-out: the number of public posts per seat-group.
- First-seen date: the first day your own snapshot recorded the posting. It is the only date you fully control.
- Requisition ID: the employer's internal identifier for the approved job, separate from the ID of any one post.
- Role family: a group of roles with similar time-to-fill, such as technical, business, or executive.
The labels replace arbitrary rules like "drop anything older than 90 days" with calls you can argue for and audit one row at a time. A colleague should be able to disagree with a label by pointing at a specific field, not at your gut.
Which date fields each careers-page ATS exposes
Greenhouse, Lever, and Ashby each answer "when was this posted" differently, so you cannot compare ages across a mix of ATSs until you re-base them on your own first-seen snapshots. An ATS (applicant tracking system) is the software behind most careers pages, and the three named here publish public job-board JSON that anyone can read.
| ATS | First-seen field | Last-edit field | Listed flag |
|---|---|---|---|
| Greenhouse | first_published (undocumented on list endpoint) | updated_at | none |
| Lever | createdAt (undocumented, creation date) | none | none |
| Ashby | publishedAt | updatedAt (authenticated API) | isListed |
What else the fields tell you:
- Greenhouse separates the post from the job. Its official sample shows
internal_job_id: 144381next torequisition_id: "50", so post ID, job ID, and requisition ID are different fields. It also returns location, departments, and offices, but it has no remote field. A practitioner write-up says the list endpoint returnsfirst_published, while the official docs only show it on the single-job endpoint. If your pull depends on it, test both endpoints on a live board. - Lever returns
createdAt, which its documentation does not list. It is the creation date, not necessarily the publication date. A requisition drafted months before it went live will look old on its first public day. - Ashby returns
publishedAt,isListed,isRemote,workplaceType,secondaryLocations, and optional compensation when you request it withincludeCompensation=true. Its authenticated API also exposespublishedDate,updatedAt, andshouldDisplayCompensationOnJobBoard, but you will not have authenticated access to a competitor's account. - Formats differ. Greenhouse uses an ISO timestamp with an offset, Ashby uses UTC, and Lever uses epoch milliseconds.
- No server-side date filter exists on any of the three. You pull the whole board every time and filter locally.
What is not established: no provider officially documents which actions reset each timestamp. The only public fact is that edits move updated_at. Treat everything else as unknown and learn it from your own daily snapshots by watching which fields change when nothing visible on the posting changed.
Four dates on one posting, most trusted first
- Your first-seen snapshotThe baseline for age; comparable across every ATS
- ATS first-publishfirst_published or publishedAt; vendor-reported go-live, partly undocumented
- ATS create or editLever createdAt or Greenhouse updated_at; drafting and editing, not posting
- JSON-LD datePostedWhat the careers page tells search engines; may refresh on repost
What each posting marker proves and how it misleads
Each public marker proves one narrow thing and misleads in a predictable way. Read the entry for the marker in front of you, apply its check, and move on.
First-publish date (first_published, publishedAt)
- Proves: when the ATS says the job first went live on the board.
- Misleads when: the field is missing from the endpoint you pulled, or a closed job is reopened as a new post with a fresh date.
- Check: compare it with your own first-seen date. If yours is earlier, trust yours.
Last-edit date (updated_at)
- Proves: someone touched the job record.
- Misleads when: it is read as freshness. A recruiter fixing a typo makes a 200-day-old seat look new.
- Check: never use it for age. Flag rows where
updated_atis more than 30 days afterfirst_published, which points to a long-running posting still being maintained.
Creation date (Lever createdAt)
- Proves: when the record was created inside the ATS.
- Misleads when: it is read as the publish date, which makes a requisition drafted months early look stale on day one.
- Check: use your first-seen snapshot as the age baseline for every Lever board.
Requisition ID and internal job ID
- Proves: which posts belong to the same approved job. This is the best dedup key when it is populated.
- Misleads when: it is blank, or an employer reuses one requisition for a standing pipeline that hires many people.
- Check: if one requisition stays up past its role-family threshold while hires appear on the team, treat it as Evergreen, not as one slow seat.
Fan-out count
- Proves: how widely one seat is being advertised.
- Misleads when: raw post counts are read as seat counts.
- Check: always count seat-groups. Use fan-out as a separate signal of urgency or reach, never as a multiplier on demand.
Fan-out varies by occupation. The ratio ran from 2.48x across all postings to 5.22x for one clinical role, so a single global dedup multiplier will misstate share of voice between functions.
JSON-LD datePosted
JSON-LD is the structured-data block a careers page embeds so search engines can read the posting, using the schema.org JobPosting type.
- Proves: what the employer declares as "the original date that employer posted the job," in Google's definition.
- Misleads when: the page refreshes it on every repost. Nobody has published how often careers pages keep the original versus refreshing it.
- Check: compare it with the API first-publish date and your snapshots. A later
datePostedon an older seat-group is a refresh flag.
JSON-LD validThrough and identifier
- Proves:
validThroughis the declared expiry. Google says it is required for postings that have an expiration date, and that if an expired posting is not removed,validThroughmust be populated and in the past.identifieris the employer's internal job ID as a PropertyValue. - Misleads when: it is absent, which is common and proves nothing.
- Check: a page still live with
validThroughin the past is a strong Stale or Ghost marker. Useidentifieras a fallback seat-group key when the API lacks a requisition ID.
Repost history
- Proves: the ad has been taken down and put back, or its dates have been reset.
- Misleads when: a reposted seat is a genuine second hire for the same role.
- Check: a repost under the same requisition with no visible hire suggests Evergreen. A new requisition suggests a new seat.
Ontario existing-vacancy statement
Under Ontario's Employment Standards Act s.8.5, an existing vacancy is "a position that is imminently available for a qualified candidate to fill." Publicly advertised postings must say whether they are for one.
- Proves: the employer has declared, on the record, that a seat exists or does not. Employers must keep posting records for 3 years.
- Misleads when: it is taken as proof. The statement is self-declared, and its absence may be lawful. The rules do not apply to employers with fewer than 25 employees on the day of posting. They exempt postings restricted to existing employees, general recruitment campaigns, help-wanted signs, and work performed outside Ontario.
- Check: confirm headcount and work location before reading absence as anything. A general recruitment campaign that lawfully omits the statement supports Evergreen. Nobody has published a compliance rate for this statement.
Pay shown where the law requires it
- Proves: the employer is treating the ad as a live, compliant posting.
- Misleads when: missing pay is read as Ghost. Ontario's pay duty only applies where expected annual compensation is $200,000 or less. In covered US states compliance is high but not universal. In one Indeed Hiring Lab snapshot, New York was at 98%, Washington at 90%, and Colorado at 88%. A secondary source citing the New York Fed puts non-compliance at about 25% of covered listings, though the primary source for that is unverified.
- Check: treat missing pay as a weak supporting marker. It is never enough on its own.
Takedown date
- Proves: the ad came down.
- Misleads when: it is read as a hire. A King County analysis found a median posting duration of 11 days, against median fill times of 39 to 76 days.
- Check: look for corroboration that someone actually joined, as described in the failure modes below.
Listed flag (isListed)
- Proves: on Ashby, whether a job "should be listed in the list of postings on your job board."
- Misleads when: unlisted rows reachable by direct URL are counted as public demand.
- Check: drop
isListed:falserows before you dedup.
How to set staleness thresholds by role family
Set staleness thresholds per role family from time-to-fill benchmarks, because nobody publishes posting lifetime by role family and a blanket cutoff misreads every family differently. Crossing the threshold triggers a review. It does not apply a label automatically.
| Measure | Business or nonexec | Technical or exec | Gap (derived) |
|---|---|---|---|
| Ashby median time to first fill | 56 days | 76 days (technical) | +36% |
| SHRM median time to fill | 39 days | 45 days (exec) | +15% |
| Median posting duration, King County | 11 days | n/a | n/a |
The SHRM and Ashby figures reach me through secondary summaries. Treat them as starting values and replace them with the competitor's own observed fill times once you have a few months of snapshots.
How to use the table:
- Assign each seat-group a role family from its department and title.
- Take that family's median fill time as the review trigger. For an engineering seat, that is about 76 days on the Ashby benchmark. For a business seat, it is about 56.
- Past the trigger, check repost history and takedown status before choosing Stale, Evergreen, or Ghost.
- Record which benchmark set the threshold, so a reviewer can challenge it.
Benchmarks drift. Greenhouse's Hire Standard reports time to fill rising from 43.64 to 59.67 over three years, and Greenhouse does not state the unit. Re-check every threshold whenever a benchmark you rely on publishes a new edition.
Age against corroboration
How to label a competitor posting, step by step
The procedure moves from raw board snapshots to one labelled row per seat-group in eight steps. Steps 1 to 6 take roughly two and a half hours of setup per board plus five minutes per row, and step 7 needs automation. No single published procedure exists. This order is assembled from practitioner write-ups on reading ATS boards.
From board snapshot to labelled seat-group
- Snapshot each rival's board dailyPull each competitor's Greenhouse, Lever, or Ashby public job board JSON and archive the raw response; done when there is one snapshot file per board per day.
- Normalize dates to first-seen and last-editConvert ISO offsets, UTC timestamps, and epoch milliseconds into one format and map each row to a first-seen and a last-edit date, or an explicit null.
- Drop unlisted rowsRemove Ashby rows where isListed is false before any counting.
- Collapse duplicates into seat-groupsGroup by requisition_id, then internal_job_id, then normalized title plus company plus location, checking Ashby secondaryLocations; keep one row per seat-group with a fan-out count.
- Cross-check structured-data datesCompare JSON-LD datePosted and validThrough against the API first-publish date and your first-seen snapshot; flag any mismatch as a refresh.
- Check jurisdictional markersRecord the Ontario existing-vacancy statement where the employer has 25 or more staff and the work is in Ontario, and pay where a covered US state requires it, noting why any absent marker is not required.
- Re-poll daily for at least 30 daysKeep snapshots running to capture repost events and takedown dates for every seat-group.
- Apply role-family thresholds and labelCompare first-seen age to the role-family threshold, assign one of the five labels with its justifying rows, and have an analyst review every non-Live label.
Notes on the steps that cause most of the arguments:
- Step 4 is where sources disagree. Aggregators key on title, company, and location. ATS readers key on IDs. I key on IDs first because title matching both merges and splits real seats (see failure modes). If you must fall back to text, one study of near-duplicate postings used a TF-IDF cosine similarity threshold of 0.95 as its dedup cutoff. TF-IDF is a word-weighting method for comparing documents. Nobody has published a benchmark comparing requisition ID, internal job ID, and description-hash keys, so spot-check 20 merged groups by hand.
- Step 7 is the bottleneck. Without automation you get no repost or takedown history, and Evergreen becomes impossible to separate from Live Seat. If nobody on your team has built a daily ATS poller, Refolk can find engineers who have shipped exactly that, with the GitHub repositories to show for it.
Use this record for every seat-group. It is the audit trail that lets someone else re-derive your label.
Company / ATS: Seat-group key: requisition_id= | fallback: internal_job_id= | fallback: title+location= Fan-out: <n> posts | locations: Role family: <technical / business / exec> | threshold: <days> | source: First-seen (own snapshot): YYYY-MM-DD ATS first-publish: YYYY-MM-DD or null Last edit (updated_at): YYYY-MM-DD or null | edit gap > 30 days: yes/no JSON-LD datePosted: YYYY-MM-DD | validThrough: YYYY-MM-DD or absent | refresh flag: yes/no Repost events: <dates, same or new requisition> Ontario vacancy statement: present / absent / not required (<25 staff, outside Ontario, general campaign) Pay shown: yes / no / not required (reason) Takedown: YYYY-MM-DD or still live | hire corroborated: yes/no/unknown Employer no-hire history: <notes> LABEL: Live Seat / Evergreen / Duplicate / Stale / Ghost Justifying rows: Reviewer and date:
One record per seat-group; write "not required" with the reason rather than leaving a marker blank.
How the label goes wrong: failure modes and false positives
Most mislabels come from reading a field as something it was never meant to be: an edit date as freshness, a creation date as a publish date, a missing marker as guilt. Each failure below has a specific check. Run the check before you accept the label.
updated_at read as freshness
A recruiter's typo fix makes a 200-day-old seat look new, and it slips into the Live Seat count. Check: always prefer first_published or first-seen, and flag rows where updated_at is more than 30 days after first_published.
Lever createdAt read as the publish date
A requisition drafted months before going live looks stale on day one and gets wrongly labelled Stale. Check: take the first-seen date from your own daily snapshots for every Lever board.
datePosted refreshed on repost
The JSON-LD shows last week while the API shows a date months earlier. Counted naively, a long-running Evergreen looks like a new Live Seat. Check: compare against API first_published and your archived snapshots. The earliest credible date wins.
Dedup on title plus company plus location
Two genuine seats with the same title in one city collapse into one, which undercounts. One seat posted as "Remote US" and "NYC" stays as two, which overcounts. Check: use requisition_id where it is populated, and Ashby's secondaryLocations to fold multi-location posts.
The vacancy statement taken as proof
"Existing vacancy: yes" is self-declared, so it supports Live Seat but does not prove it. Its absence may only mean the employer has fewer than 25 staff or the work is outside Ontario. Check: confirm headcount and work location before inferring anything, and remember that general recruitment campaigns are exempt.
Missing pay read as Ghost
Ontario's pay duty stops above $200,000, and roughly a quarter of covered US listings omit pay anyway, per a secondary source. Check: treat missing pay as a weak supporting marker only.
Takedown read as a hire
Ads come down long before fills: an 11-day median posting duration against 39 to 76 day fill medians. A takedown can mean a hire, a freeze, a repost elsewhere, or a cancelled requisition. Check: look for a person who joined the competitor in that role after the takedown. Refolk returns people by title and employer across LinkedIn and the open web, which turns "did they actually hire a staff data engineer in Toronto" into one search instead of an afternoon.
A takedown date tells you an ad came down, not that a seat was filled.
A published ghost rate applied to one competitor
Greenhouse's 18 to 22% counts no-hire outcomes, including frozen requisitions, not intent, and it covers that platform's customers only. Check: never discount a competitor's count by a platform-wide percentage. Measure the employer's own history.
One blanket 90-day cutoff
Against Ashby's 76-day technical median, a 90-day rule flags normal engineering seats as stale while letting 60-day-old business roles, which typically fill around 56 days, through as live. Check: use role-family thresholds and record the benchmark behind each one.
What published ghost-job rates can and cannot tell you
Published ghost-job rates tell you ghosting is common and concentrated in a minority of employers. They cannot tell you whether a given competitor ghosts. Use them to set your prior, then replace them with the competitor's own record.
What the figures say:
- Greenhouse classified 18 to 22% of postings on its platform as ghost jobs per quarter. Nearly 70% of its client companies posted at least one in a single quarter.
- Concentration: for about 15% of Greenhouse's clients, posting ghost jobs was a regular occurrence, and half of the jobs this group listed went unfilled.
- Sector: construction was highest at 38%.
- Definition: a secondary analysis describes the classification as postings that produced no hire, which sweeps in frozen and cancelled requisitions alongside deliberate fakes.
- Gaps: nobody has published a breakdown by seniority or company size.
Two implications for competitive intelligence:
- Ghost risk clusters by employer, not by posting. An employer-level history of postings with no visible hire beats any per-ad heuristic. After 30 days of snapshots, compare each rival's takedowns against corroborated hires. A rival in the regular-ghosting minority deserves a stricter bar for Live Seat.
- Market-level counts overstate evergreen-heavy employers. One major aggregator matches normalized title, company, and location within a 60-day window and claims to catch about 80% of duplicates. It treats a re-ad after 60 days as a new posting, so an ad reposted continuously for a year counts 6 times. The same researchers warn the method may filter a lower share of duplicates as the number of scraped sites rises. If you benchmark a rival against aggregated market data, its Evergreen ads may be inflating the comparison.
Keeping the labels current
Labels decay: a Live Seat becomes Stale in weeks, laws change which markers are required, and benchmarks drift. Keep the daily poll running, re-label every seat-group on each refresh, and re-check the rules behind your thresholds on a fixed calendar.
What to re-check, and how:
- Jurisdictional rules. Ontario's thresholds (25 employees, $200,000 pay cap) and US state pay laws change. Re-read the Ontario ESA guide's recent-changes page (O. Reg. 476/24) and the Indeed Hiring Lab pay-transparency dataset on GitHub before each quarterly brief.
- Fill benchmarks. Replace the Ashby and SHRM medians with the competitor's observed fill times once you have them.
- Timestamp behaviour. Because no ATS documents what resets each field, watch for silent changes in your snapshots and log them.
The people who do this work are rarely a team. In Refolk's index of professional profiles, the exact title "Talent Intelligence" returns 3 people in the United States, 1 in the United Kingdom, and 0 in Canada.
Read that as a floor, not a census. Exact titles miss analysts working under strategy, people-analytics, or recruiting-operations titles. The practical point holds anyway: the person who labels postings is often alone, so the label record has to be good enough for someone else to audit. To find peers to compare methods with, search Refolk with broader phrasing, such as talent intelligence analysts at Netflix, Google, or JPMorganChase in the United States, and widen from there.
Before a posting count goes into a competitor brief
- Every row has a first-seen date from my own snapshot, not only an ATS date.
- No age was computed from updated_at or Lever createdAt.
- Ashby rows with isListed false were removed before dedup.
- Each seat-group was keyed on requisition_id or internal_job_id where populated, with its fan-out count recorded.
- JSON-LD datePosted and validThrough were compared against the API date, and mismatches carry a refresh flag.
- Every absent vacancy statement or pay range has a recorded reason: required and missing, or not required.
- Snapshots covered at least 30 days, so repost and takedown history exists for each seat-group.
- Each staleness threshold names its role family and its benchmark source.
- No Ghost label rests on a single marker or on a platform-wide ghost rate.
- Only Live Seats are in the velocity number, and Evergreen demand is reported separately.
Questions practitioners ask
How do I tell if a competitor job posting is a ghost job?
You cannot tell from one ad, so require at least two independent markers plus employer history before labelling anything a Ghost. Strong markers include a validThrough date in the past on a still-live page, repeated reposts with no seat-group change, and a missing Ontario vacancy statement where the employer has 25 or more staff and the work is in Ontario. An employer with a track record of postings that never produce hires is the strongest single context signal.
What is the difference between first_published and updated_at in Greenhouse?
first_published is when the job first went live on the board, while updated_at changes whenever a recruiter edits the job. That means updated_at is not a posting date and can make a months-old seat look brand new after a typo fix. Use first_published for age, and flag rows where updated_at is more than 30 days after first_published. Note that first_published is documented on the single-job endpoint, not the list endpoint.
How do I tell an evergreen job posting from a real open seat?
An evergreen posting stays open or is reposted well past the normal fill time for its role family, usually with a generic title and no change in requisition ID. Watch its repost history across at least 30 days of daily snapshots and compare its age to role-family medians such as Ashby's 56 days for business and 76 days for technical roles. In Ontario, a general recruitment campaign may lawfully omit the existing-vacancy statement, which supports the Evergreen label.
Should I drop competitor job postings older than 90 days?
No, because one blanket cutoff misreads different role families. Against Ashby's 76-day median time to first fill for technical roles, a 90-day rule flags normal engineering seats as stale while letting 60-day-old business roles, which typically fill around 56 days, pass as live. Set a threshold per role family from time-to-fill benchmarks, treat crossing it as a review trigger, and document the source of each threshold.
Can I apply Greenhouse's ghost job rate to a specific competitor?
No. Greenhouse's 18 to 22% quarterly figure classifies postings that produced no hire on its own platform, which sweeps in frozen and cancelled requisitions alongside deliberate fakes. It describes that platform's customers in aggregate, not any one employer's intent. Ghosting also clusters by employer, with about 15% of Greenhouse clients doing it regularly, so measure the competitor's own posting-to-hire history instead.
How do I deduplicate competitor job postings posted in several locations?
Key on requisition_id where it is populated, fall back to internal_job_id, and only then use normalized title plus company plus location. Title-plus-location matching merges two genuine seats with the same title in one city and keeps one seat posted as Remote US and NYC as two. On Ashby, read secondaryLocations to fold multi-location posts into one seat-group, and keep the fan-out count on the surviving row.
Try it on the search you came here for
Stop building boolean strings. Just describe the person.
Type one sentence. I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web as it is right now, and hand back a ranked list with the reason next to every name.
01Describe them
One plain sentence. Role, city, stack, stage, whatever matters to you.
02I read the web live
GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.
03You read the shortlist
Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.
- Staff backend engineers in NYC who shipped Rust in production
- Series A fintechs in SF under 50 people, growing headcount this year
- Maintainers of fast-growing Rust web frameworks on GitHub
- No boolean, no filters, no seat to buy. One box.
- Read at search time, so a profile updated yesterday counts today.
- Every step visible as it runs, every name with its reason.
500 free credits on sign-up. No card, no demo call. See real searches.