Pay range or no post: a pre-publish check for job ads

A recruiter copies a client's job description into the ATS, hits publish, and it lands on your careers feed, Indeed and LinkedIn inside an hour. If that req is performed in Illinois, Minnesota, New Jersey, Vermont or Massachusetts, and there is no pay range in the ad, you have a problem that is yours and not the client's. Most of these statutes reach third parties who post on an employer's behalf, which is the entire staffing business model.

This is a good candidate for a small agent for the same reasons VMS intake is: the volume is daily, the rule is written down, and the failure is silent until someone complains. It is also not a legal opinion generator. The job is to check a posting against a table your counsel signed off on, block the ones that fail, and keep a record of what went out.

The current shape of the rules

As of early 2026 the state-level list a US staffing firm hits most often looks roughly like this. Thresholds are employee counts, and they vary; confirm each line with counsel before you encode it, and re-check quarterly, because cities keep adding ordinances.

JurisdictionIn forceEmployer thresholdUsual ask
Colorado2021anyRate or range, benefits summary, application deadline
New York StateSept 20234+Range, job description
CaliforniaJan 202315+Pay scale in the posting
WashingtonJan 202315+Range plus general benefits
IllinoisJan 202515+Range plus benefits, and third-party posters are covered
MinnesotaJan 202530+Range or fixed rate, benefits description
New JerseyJune 202510+Range plus benefits
VermontJuly 20255+Range
MassachusettsOct 202525+Range

Plus city ordinances (Cincinnati, Toledo, Jersey City, Cleveland from late 2025) and salary-history bans that often travel with them. The point of the table is not that it is complete. The point is that it lives in a database rather than in a recruiter's memory.

create table posting_rules (
  id                 bigserial primary key,
  jurisdiction       text not null,        -- 'US-IL', 'US-NY', 'US-OH-CLEVELAND'
  effective_from     date not null,
  effective_to       date,
  employee_threshold int,                  -- null = applies to all
  requires_range     boolean not null default true,
  requires_benefits  boolean not null default false,
  requires_deadline  boolean not null default false,
  bans_salary_history boolean not null default false,
  citation           text not null,        -- statute or ordinance, as cited by counsel
  reviewed_on        date not null,
  reviewed_by        text not null
);

reviewed_on and reviewed_by are not decoration. When a client asks why you posted a bare title in Newark last March, the answer is the row as it stood on that date, with a name against it.

Step one: resolve the jurisdictions for a req

This is the part everyone underestimates. A req is not in one place. It has a work location, sometimes several; it may be hybrid against an office; it may be remote-anywhere, in which case several states take the view that their rules apply if the work can be performed there.

Resolve to a set, not a value:

create table req_jurisdictions (
  req_id        text not null,
  jurisdiction  text not null,
  basis         text not null,   -- 'worksite', 'remote-eligible', 'supervisor-location'
  resolved_at   timestamptz not null default now(),
  primary key (req_id, jurisdiction)
);

Three resolver rules cover most of it. A named worksite adds its state and city. A remote-eligible req adds every jurisdiction in your rules table unless the client has excluded states in writing, in which case store the exclusion list on the req and add only the rest. An onsite req whose hiring manager sits elsewhere may add the supervisor's state; Illinois is the one that made people notice this.

Then the requirement for the posting is the union of the requirements of every resolved jurisdiction. Strictest wins. In practice that means most remote reqs need a range, a benefits line and, if Colorado is in the set, an application deadline.

Step two: gate the publish, do not chase after

The agent runs at the moment a recruiter tries to publish, not nightly. Nightly means the ad was live and wrong for nine hours.

The check itself is dull, which is the aim:

  • Is there a pay_min and pay_max on the req, or a fixed rate, in structured fields rather than prose? Prose ranges parse badly and paste worse.
  • Is the range plausible? Flag anything wider than a factor your firm agrees on, say 1.8x, or a floor under the applicable minimum wage. "$1 to $500,000 per hour" is technically a range and has already been litigated about elsewhere.
  • Does it carry the benefits sentence, if any resolved jurisdiction wants one?
  • Does the description body contain salary-history questions, in a jurisdiction that bans them? A short pattern list catches most: "current salary", "salary history", "present compensation".
  • Does the bill rate imply a pay range you cannot actually honour? This one is internal, but it is the one that saves margin arguments later.

Failures split two ways. Missing range in a covered jurisdiction is blocking: the publish button does not work, and the agent names the jurisdiction and the field to fill. Everything else is advisory: it posts, with a flag on the req and a line in the daily exceptions list.

Keep the model out of the blocking path. An LLM is useful for reading a client's pasted description and proposing which fields it implies, and for drafting the benefits sentence in the client's voice. It should not be the thing that decides whether a posting is lawful. Deterministic rule, human override, recorded.

Step three: record what actually went out

Boards mangle postings. Your ATS feed sends a range field that Indeed renders and LinkedIn sometimes does not, and syndication partners re-post stale copies for weeks. So store the artifact, per destination:

create table posting_events (
  id             bigserial primary key,
  req_id         text not null,
  destination    text not null,      -- 'careers-site', 'indeed', 'linkedin', 'client-portal'
  posted_at      timestamptz not null,
  removed_at     timestamptz,
  pay_min        numeric(10,2),
  pay_max        numeric(10,2),
  pay_unit       text,
  rules_applied  jsonb not null,     -- jurisdictions + rule ids as at posted_at
  body_snapshot  text not null,
  actor          text not null,      -- the person who published
  override_reason text
);

rules_applied freezes the rule versions used, so a later rule change does not rewrite history. body_snapshot is what a regulator or a plaintiff's lawyer will ask for, and it is cheap to keep. Illinois expects records for a number of years; pick your longest applicable retention, put it in the purge job, and do not keep candidate PII in this table at all — it is about ads, not people.

Overrides deserve their own field. A recruiter will hit a blocking failure at 4:55pm with a client on the phone, and someone senior will wave it through. Better that the wave-through is a row with a name and a reason than a disabled check.

What to measure after a month

  1. Share of new postings with a structured range, before and after.
  2. Blocking failures caught at publish, by jurisdiction. This is the number that tells you which client intake is sloppy.
  3. Overrides, with reasons, and who signed them.
  4. Time from req creation to live posting. If the gate added more than a couple of minutes, your intake form is missing fields, not your recruiters.

The second number usually surprises owners. Firms that believe they are clean typically find a fifth to a third of reqs arriving from clients with no rate at all, which is a client intake problem the agent has just made visible.

When not to build this

If you post exclusively in states with no transparency rule and have no remote reqs, a checklist in the intake form is enough. Not yet.

If you post across five or more states, take remote reqs, or syndicate to boards you do not control, the gate pays for itself the first time a client sends a bare title for a New Jersey role. It is also a two-week build in most ATSs, assuming pay fields exist and are not free text — on Loxo, Crelate, JobAdder, Vincere and Recruiterflow they usually do, though what the feed actually transmits to each board is worth testing before you trust it.

One warning. Nothing here makes a posting lawful, and no tool can. It enforces a table your counsel wrote and produces a record of what you published. That is the part engineering can own.

If you want it built, contact us with your ATS, the states you post in and a month of postings, and we will tell you what share would have failed the gate. A Workflow Assessment ranks it against the rest of the queue.