Most staffing firms that work MSP programs get their reqs the same way: a distribution email lands in a shared inbox from Fieldglass, Beeline, SimplifyVMS or a program office running spreadsheets, and someone re-keys it into the ATS. The req has a submission deadline measured in hours, a rate ceiling, a location, and a job id that belongs to the portal rather than to you. Miss the deadline and the submission window closes; re-key the rate wrong and you find out at invoice time.
This is the kind of work an ATS vendor feature will not do for you, because the data starts outside the ATS. It is also a good first agent: the volume is daily, the rules are written down, and the payback shows up as submittals made before the window closed.
We are not building a portal scraper here. Scraping a VMS UI violates most program agreements and breaks every time the vendor ships a release. We work from the email the program already sends you, plus whatever export or API access your program office grants in writing.
Step one: pin down what a req actually is
Before any parsing, write the target record. Portal reqs are not ATS jobs, and pretending they are is what makes the dedupe mess later.
create table vms_reqs (
id bigserial primary key,
program text not null, -- 'fieldglass:acme', 'beeline:northwind'
external_req_id text not null, -- the portal's id, as printed
title text,
client_name text,
location text,
bill_rate_max numeric(10,2),
rate_unit text, -- 'hour', 'day'
submission_limit int, -- candidates you may submit
submit_by timestamptz,
received_at timestamptz not null,
source_message_id text not null,
raw_email text not null,
ats_job_id text, -- filled when a person creates the job
status text not null default 'new',
unique (program, external_req_id)
);
Two fields earn their place. program is scoped per client because external req ids collide across programs; a bare external_req_id unique constraint will silently merge two clients' reqs within a month. And raw_email is kept because in three weeks someone will argue about what rate the program actually sent.
Step two: parse deterministically first, model second
VMS distribution emails are templated. Fieldglass and Beeline notifications for a given program look the same every time until the program office changes the template, which happens perhaps twice a year. So run a rules pass first:
const RULES = {
'fieldglass:acme': {
externalReqId: /Request ID:\s*([A-Z0-9-]+)/,
title: /Job Title:\s*(.+)/,
billRateMax: /Max(?:imum)? Rate:\s*\$?([\d,]+\.?\d*)/,
submitBy: /Respond(?: by)?:\s*(.+)/,
},
};
function parseRules(program, text) {
const spec = RULES[program];
if (!spec) return null;
const out = {};
for (const [field, re] of Object.entries(spec)) {
const m = text.match(re);
if (m) out[field] = m[1].trim();
}
return out;
}
Regexes are cheap, free to run, and auditable. Send the email to a model only when the rules pass comes back missing a required field, and make the model return the same shape with a confidence flag rather than prose:
{
"external_req_id": "REQ-88214",
"title": "Senior Network Engineer",
"bill_rate_max": "78.00",
"rate_unit": "hour",
"submit_by": "2026-09-22T17:00:00-04:00",
"submission_limit": 2,
"fields_uncertain": ["submission_limit"]
}
Anything in fields_uncertain goes to a human before the job is created. So does any rate above the client's normal band, which is the field that costs real money when it is wrong.
Step three: time zones, because deadlines are the point
A submission deadline is the whole value of this agent, and it is the easiest thing to get wrong. Three rules:
- Store
submit_byastimestamptz, always. Never store the string the email printed. - Resolve the zone from the program configuration, not from the email body. "5:00 PM" in a Fieldglass notification usually means the program's zone, not the req location's, and not yours.
- Refuse to guess. If the zone cannot be resolved, mark the req for a person and say why. A deadline that is four hours wrong is worse than a deadline that is missing, because nobody checks the one that looks fine.
Then alert on the clock, not on arrival. A req received at 4 pm Friday with a Monday 9 am deadline needs a Monday 7 am nudge, not just the Friday one.
Step four: dedupe against the ATS before you create anything
The same req reaches you three ways: the portal email, a direct forward from the account manager, and a re-send when the program office edits the description. Match on (program, external_req_id) first. When that is absent, fall back to a fuzzy check and route it to a person rather than deciding:
select id, external_req_id, title, client_name, received_at
from vms_reqs
where client_name = $1
and received_at > now() - interval '14 days'
and similarity(title, $2) > 0.6
order by received_at desc;
An edited re-send should update the existing row and log the change, not create a second job in the ATS. Recruiters trust a req list exactly as long as it has no duplicates in it.
Step five: leave job creation to a person
The agent's output is a queue: parsed req, source email one click away, a diff if it is an update, and a Create in ATS button that writes the job through the ATS API and stores the returned id in ats_job_id. The human check takes about fifteen seconds and it is what keeps a bad parse out of the system of record. Same boundary we draw everywhere else: the agent drafts and flags, a named person commits.
Log every creation with the actor, the timestamp and the payload sent. When a client later asks why you submitted against a rate you were never offered, that log is the answer.
What to measure after a month
Four numbers, all countable from the tables above:
- Reqs parsed with no human correction, as a share of reqs received.
- Median minutes from email receipt to job created in the ATS.
- Submittals made before the window closed, before and after.
- Duplicate jobs created. This should be zero; if it is not, the dedupe rule is wrong, not the recruiters.
The first number tells you whether the parser is healthy. The third is the one to put in front of the owner, because it is the one that shows up in gross margin.
When not to build this
If you take five program reqs a week, a person re-keying them is cheaper than a pipeline someone has to maintain through two template changes a year. Not yet. The threshold we usually see is somewhere north of twenty reqs a week across two or more programs, or one program whose deadlines you are demonstrably missing.
If you are over that line, contact us with your ATS, your programs and a week of those emails, and we will tell you which of them parse cleanly. A Workflow Assessment ranks it against whatever else is eating your ops hours.