hyreflow Native
First-party tools metered in hyreflow credits with no separate subscription: job scrapes across career pages, LinkedIn, Indeed, Arbeitsagentur and StepStone (launch, then poll, charged per job returned), LinkedIn post feeds for a person or a company, keyword search across LinkedIn posts scoped to a date window the people who reacted to a post, and a LinkedIn profile lookup by URL that feeds the linkedin_profile waterfall (one call, charged per request), a free postcode-radius lookup that turns 'within N miles of a postcode' into a town list for people search, a repost scan that finds a company's hard-to-fill roles in its PredictLeads job history, and email verification (an inbox-level bulk check that returns only the addresses safe to send to, charged per address submitted, at launch).
Reach for hyreflow Native on four triggers. "Find the open roles", pull everything a company is hiring for off its career site, or search LinkedIn / Indeed / the German federal board / StepStone by title and location. "What have they been posting?", read a person's or a company's recent LinkedIn posts for launch, hiring, funding, exec-commentary and layoff signals. "Who is posting about this?", search every public LinkedIn post from the past week for a keyword ("we're hiring recruiter"), narrowed to authors whose headline matches, when you don't yet know whose feed to read. "Are these emails good?", verify addresses before a send, the first choice among hyreflow's email verifiers. First-party tools you run on hyreflow credits: no scraper subscription, no API key to manage.
Native (hyreflow-metered, no BYOK): first-party capability billed in hyreflow credits. Profile lookup is the second step of the linkedin_profile waterfall; every other native tool runs on its own.
Job scrapes and post feeds work differently, and they bill differently:
| Shape | Billing | |
|---|---|---|
| Job scrapes | Asynchronous: launch, then poll | Per job delivered; the launch is free, and an empty result costs nothing |
| Post feeds | A single call returns a page of posts | Per request, one page of up to 50 posts, whatever the page carries |
| Post search | A single call returns a page of matching posts | Per request, one page of up to 10 posts, a keyword with no hits included |
| Post reactions | A single call returns a page of reactors | Per request, one page of up to 10 reactors, an unreachable post included |
| Profile lookup | A single call returns one profile | Per request, a URL that resolves to no profile included |
| Email verification | Asynchronous: launch, then poll | Per address submitted, charged at launch; polling is free |
| Repost scan | A single call reads a company's job history | 0.8 credits/page for each PredictLeads history page it pulls (one per domain, up to three for a domain past 1,000 listings); the scan itself adds nothing |
Job scrapes: launch, then poll
A scrape takes a few minutes, so every job scraper is a two-step flow:
- Launch with a
scrape_*tool (or its short alias) → returns{ request_id, status: "QUEUED" }. Free. - Poll the matching
get_*tool with thatrequest_id, from the same workspace that launched it: arequest_idanother workspace launched (or one Hyreflow never issued) reads the same as one that doesn't exist,404.statusmovesQUEUED→RUNNING→COMPLETED. A LinkedIn run can also endPARTIAL: it finished, butresult.failed_scrapespostings could not be loaded (result.errorslists them when upstream names them).PARTIALis terminal, so stop polling; itsresult.jobsbills likeCOMPLETED, and the failed postings aren't in it, so they cost nothing. WhenCOMPLETEDorPARTIAL,resultcarries a page ofjobsplusjobs_total,jobs_returned,limit,offset,has_more, andwarnings. Some sources also reportjobs_filtered_out; LinkedIn and Indeed don't, because their filters run upstream. Poll roughly every 10s: re-polls dedup per job, so a tighter loop never costs extra, it only shortens the wait.
You're charged once per unique job delivered: each job in a scrape is billed exactly once. Re-polling, widening the limit, or overlapping page windows never double-charge; paging forward bills only the newly-delivered jobs.
Because a launch is free and returns immediately, you don't have to run scrapes one at a time. Launch several scrape_* calls up front, collect their request_ids, and poll them together, launches aren't serialized, and a batch of around 20 runs is a practical working size. A multi-angle sweep (the same titles across career pages, LinkedIn, Indeed, and Arbeitsagentur, or the same query across several cities) then costs roughly one scrape's wall-clock instead of the sum of every scrape run sequentially. Each run in the batch bills its own delivered jobs as you poll it, so a wide batch commits that spend at once, size the batch accordingly.
jobs_returned is only the current page: with the default limit of 10, a first poll showing 10 jobs says nothing about how many roles the scrape actually found. Check jobs_total and has_more to judge coverage before deciding you need another scrape.
Job scrapers
| Launch (free) | Alias | Poll for results | Scrapes |
|---|---|---|---|
scrape_career_pages | career_pages | get_career_pages | A company's career site + its ATS (Greenhouse, Lever, Workday, Ashby) |
scrape_linkedin_jobs | linkedin_jobs | get_linkedin_jobs | LinkedIn jobs for one company's LinkedIn page, by title plus location, or specific postings by URL or id |
scrape_indeed_jobs | indeed_jobs | get_indeed_jobs | Indeed jobs by title + location (regional hosts) |
scrape_arbeitsagentur_jobs | arbeitsagentur_jobs | get_arbeitsagentur_jobs | Bundesagentur für Arbeit (German federal job board) |
scrape_stepstone_jobs | stepstone_jobs | get_stepstone_jobs | StepStone (Germany's largest commercial job board) |
scrape_arbeitsagentur_jobs only returns recruiter details with include_recruiter_details: true (slower). The poll charges +0.1 credits per job with recruiter details, up to 0.15 credits per job including the per-job rate; other jobs bill the per-job rate only, and re-polling never re-charges it.
The get_* pollers accept limit/offset (default 10, max 50) to page through result.jobs. A limit above 50 is rejected rather than quietly trimmed, so a paging loop can't silently skip the jobs a shortened page would have left behind. Advancing to a new page bills the jobs that page delivers. Dedup makes re-polling a window you already fetched free, it does not make the next window free, so has_more: true is not a reason to keep going. Stop once you have what was asked for; to walk the whole result deliberately, keep incrementing offset by limit and re-polling while result.has_more is true.
get_linkedin_jobs and get_indeed_jobs also take fields (a list of job field names to keep) and max_description_chars (truncates descriptions), applied to the page after it is fetched. They shrink what lands in your context and never change the charge. A fields name that is not a job field of that source (and that no job on the page carries) is refused before the page is billed, and the error lists the valid names. A valid name a job doesn't carry, such as salary, is just absent from that job, so the same fields works on every page. On the first page of a finished poll, upstream failed_scrapes, errors and partial appear in _meta.warnings, with the first few upstream warnings; later pages don't repeat them. On a partial LinkedIn run whose result.errors lists the failed postings by URL or id (at most 100 per argument), relaunch only those as linkedin_job_urls / linkedin_job_ids on their own, once (re-running the whole search starts a new run, so every job already delivered bills again). limit is the poll's page size (max 50); rows is the scrape's size on the launch, so page a larger LinkedIn run with offset. Indeed's own first page is about 15 jobs, and a run the second source tops up (best effort, see "Indeed depth" below; listing_source indeed+secondary) returns up to about 50 per location, so an Indeed run asking for more than 50 can need a second poll: check has_more.
Post feeds: one call, one page
A post feed is not asynchronous: one call returns the posts inline, newest first:
| Tool | Alias | Reads |
|---|---|---|
get_profile_posts | profile_posts | A person's recent posts, from the handle in linkedin.com/in/<username> |
get_company_posts | company_posts | A company page's recent posts, from the handle in linkedin.com/company/<username> |
One request returns one page of up to 50 posts and is charged as one request. To walk further back, pass the next_start the previous call returned while has_more is true, each page is another request, so it is another charge. limit trims how many posts come back (handy when you only want the last handful in context); it does not change what a request costs, so there is no saving in asking for fewer, and it is not the paging step, which is why next_start is the value to advance on.
Every post carries its full text, the post URL, when it was posted (both a timestamp and a date), reaction / comment / repost counts, the content type, the author, and any companies tagged in it. A company feed also reports total and total_page for the whole feed.
A request is charged whether or not the handle resolves, a page of 50 posts and an unreachable handle cost the same. When a handle can't be read, success is false and message says why, so check the handle against the profile URL before spending a request on it.
# a person's recent posts — one call, one charge
hyreflow tools execute profile_posts --payload '{"username":"acme-dana-reed","limit":10}'
# a company page's feed, second page
hyreflow tools execute company_posts --payload '{"username":"acme","start":50}'Post search: one keyword, one page
search_posts (alias post_search) finds public LinkedIn posts by what they say rather than who said them. It is the tool for a hiring signal you can't attach to a handle yet: recruiting-agency staff announcing a search, a hiring manager posting "we're hiring", a founder mentioning a new office.
| Argument | Default | Meaning |
|---|---|---|
keyword | required | Text the post must contain; a phrase is fine, e.g. "we are hiring" |
date_posted | past-week | past-24h, past-week, past-month, or any to drop the window |
author_title | none | Only posts by authors whose headline matches, e.g. "Recruiter", "Talent Acquisition" |
from_members | none | A list of profile handles to restrict to |
sort_by | date_posted | date_posted (newest first) or relevance |
page | 1 | 1-based page; walk forward with the next_page the previous call returned |
limit | 10 | Trims how many posts come back (a page holds at most 10), never what the request costs |
A search is scoped to the past week by default. A hiring post is only a signal while the role is open, so an unscoped search is rarely what you want; when it is, pass date_posted: "any" deliberately. Every result is the same post record a feed returns (text, URL, dates, engagement counts, author, tagged companies), so the two read alike downstream. The response reports total for the whole match set alongside page, page_size, has_more, and next_page.
A page is 10 posts, so one request is one page of up to 10 hits and one charge, and a keyword nobody posted about this week is charged the same as a full page. Narrow with author_title before paging: "hiring" matches everything, "hiring" from a "Recruiter" is the agency-staff signal in one request.
How the window is applied depends on the sort. With sort_by: "relevance" the window filters the match set and total counts only posts inside it. With the default sort_by: "date_posted" results stream newest-first and the window is applied to that stream: total counts the whole unwindowed match set, and the walk ends (has_more: false, next_page: null) on the first page that reaches past the window, so you never pay for a page of stale posts. An empty page whose message reads "No matching posts returned (the search source reported no result twice)" is a transient upstream miss rather than a real zero: the request was already retried once automatically; re-run the same call. It is still charged like any other request.
# recruiting-agency staff announcing a search this week — one call, one charge
hyreflow tools execute search_posts \
--payload '{"keyword":"we are hiring","author_title":"Recruiter"}'
# widen to a month, next page
hyreflow tools execute search_posts \
--payload '{"keyword":"talent acquisition partner","date_posted":"past-month","page":2}'Post reactions: who engaged with a post
get_post_reactions (alias post_reactions) reads the people who reacted to a LinkedIn post — the
engagement list a feed row's totalReactionCount only counts. Pass the post's postUrl (or any
linkedin.com/posts/... link); one call returns one page of up to 10 reactors and is charged as one
request, whether or not the post resolves. total and total_pages report the whole reaction set, so a
heavily-reacted post is a paged read — advance with next_page (null at the end). limit trims what
comes back, never what a request costs.
Each reactor carries fullName, headline, profileUrl, urn, profileType and the reactionType
they left. reaction_type filters to one kind — LIKE, EMPATHY, PRAISE, INTEREST, FUNNY or
APPRECIATION; omit it for all reactions.
# who reacted to a post — one call, one charge
hyreflow tools execute post_reactions \
--payload '{"url":"https://www.linkedin.com/posts/acme-dana-reed_we-are-hiring-740001"}'
# next page, likes only
hyreflow tools execute post_reactions \
--payload '{"url":"https://www.linkedin.com/posts/acme-dana-reed_we-are-hiring-740001","page":2,"reaction_type":"LIKE"}'Profile lookup: one URL, one profile
get_linkedin_profile reads one person's LinkedIn profile from its URL (linkedin.com/in/<handle>; a bare handle also works). It is the second step of the linkedin_profile waterfall, after AI Ark, so for work history across a list run linkedin_profile and let the waterfall pick; call it directly for one person's full record.
The response is {success, profile, message}. profile carries the name, headline, about, location, open-to-work and hiring flags, experience (newest first, each with company, title, start and end as YYYY-MM, is_current, location, employment type and the company's LinkedIn URL, industry and size), education, skills, certifications and languages. It carries no email or phone, so it is safe to run on candidates.
A URL that resolves to no profile returns success: false and profile: null, and is charged as one request like any other. Inside the linkedin_profile waterfall a miss costs nothing and the chain moves on.
When the lookup itself is unavailable (its allowance is used up, it is refusing calls, it is not set up, or it timed out), a direct call runs the linkedin_profile waterfall for that URL without this source and answers in the same shape, plus source (the provider that answered) and fallback (from, reason, charged). It is charged what the waterfall charges: the answering provider's price when a profile is found, plus any source on the way that bills a miss (fallback.charged). Fields only this source carries (username, URN, first and last name, country, photo, the open-to-work and hiring flags, languages, a role's location, employment type, company page, industry and size, and a school's field of study and dates) come back empty. A "no profile" answer never falls back, and neither does a source that is only busy (wait and retry). The fallback stays within the credits you approved, or, if you approved none, within the amount Hyreflow would otherwise ask you to confirm, counted across every fallback in the request (lookups that answered from this source don't count): past that, the call returns the error, which tells you to use linkedin_profile.
# one person's profile — one call, one charge
hyreflow tools execute hyreflow_native get_linkedin_profile \
--payload '{"linkedin_url":"https://www.linkedin.com/in/acme-dana-reed"}'Postcode or city radius: towns within N miles
People databases match a person's location by exact town name and have no radius filter. places_within_radius (alias postcode_radius) turns a postcode or a city and a radius into every city, town, village and suburb inside it, nearest first, as a ready-made locations list for people_search. It is free.
- The centre, exactly one of:
postcode: a full postcode or ZIP (SW1A 1AA,94103,10115). A bare UK outcode such asSW1Ais not found.place: a city, town or village name (Marburg). It matches the settlement only, soNew Yorkis the city, not the state.
radiuswithunitmi(default) orkm, up to 25 km (about 15 mi);countryas an ISO-2 code (gb,de,us).limit: the nearest places to keep, 50 by default and at most 200.- Returns
center,radius_km,places(name,type,distance_km),locations(led by the centre's own town),total_placesandtruncated.
hyreflow tools execute postcode_radius --payload '{"postcode":"SW1A 1AA","radius":3,"country":"gb"}'
hyreflow tools execute postcode_radius --payload '{"place":"Marburg","radius":20,"unit":"km","country":"de"}'Profiles carry a town, not a postcode, so a match means the person lists a town inside the circle. Keep the metro label in the list you pass on: most people list the metro, not their suburb. Place data © OpenStreetMap contributors (ODbL).
Email verification: only safe-to-send addresses come back
The default email verifier on hyreflow: 0.05 credits per address submitted. verify_emails checks 1 to 50,000 addresses, each down to the mailbox, and hands back only the ones safe to send to.
Email enrichment and personal email enrichment already run this check on every address they find: an address that isn't safe to send to is dropped before it reaches your row, and the waterfall tries the next provider. An address on a catch-all domain, or one the verifier can't judge, gets a second check from Enrichley, and is kept only if Enrichley clears it. Use verify_emails for addresses from anywhere else.
verify_emails returns { request_id, status: "QUEUED", submitted, duplicates_removed, rejected }. Poll get_verified_emails with that request_id roughly every 10s until status is COMPLETED (or FAILED). result.emails holds the addresses safe to send to (is_safe_to_send: true). Every other address (invalid, catch-all, role account such as info@, disposable, full or disabled inbox, spam trap, or no verdict) is withheld and never returned as a result; result.withheld counts them. Two kinds are listed by address only, in case you want a second opinion on them: result.catch_all (the domain accepts any mailbox, so this one can't be confirmed) and result.unknown (no verdict). Page with limit (1–1000, default 100) and offset, advancing offset by limit while has_more is true.
You're charged at launch for every address submitted, whatever its verdict; duplicates and malformed addresses the launch drops are free. Polling for the results is free. The launch is refused with 402 unless your balance covers every submitted address.
To use a different validator (Enrichley, LeadMagic, Icypeas), ask for it by name.
# launch (charged per address), then poll (free)
hyreflow tools execute verify_emails \
--payload '{"emails":["[email protected]","[email protected]"]}'
# → { "request_id": "ev_...", "status": "QUEUED", "submitted": 2, ... }
hyreflow tools execute hyreflow_native_get_verified_emails \
--payload '{"request_id":"<id>"}'Repost scan: hard-to-fill roles
repost_scan reads a company's PredictLeads job-opening history and returns only the roles that show a hard-to-fill signal, each with its listings as evidence:
repost: a listing first seen in the lastfresh_daysdays, after an earlier listing of the same role at the same site ran 21 days or more and stopped.long-open: the role has been live 90 days or more.evergreen: three or more back-to-back relists about a month apart, the latest still live.
Listings are the same role when the titles match once seniority words and gender tags are dropped, and they're in the same town. Apprenticeship, student and intern listings never count. Pass related_domains for the company's parent or subsidiary domains, and titles or onet_codes to check one role. Pairs the titles can't settle, such as a renamed role, come back in needs_review so you can compare their descriptions. warnings says when a history was cut off or the crawl data is stale; then a missing signal isn't a "no".
The scan itself is free. Each history page it pulls is one PredictLeads job_openings request, billed at that rate (one page per domain holding up to 1,000 listings, up to three pages for a domain whose history runs past that), and free on your own PredictLeads key.
hyreflow tools execute hyreflow_native repost_scan --payload '{"domain":"acme.example.com","titles":["Account Executive"],"fresh_days":30}'Guidance
- Career pages: narrow with
target_titles(a list) ortarget_titles_prompt(a plain-English filter), use one, not both. - LinkedIn / Indeed: pass
titles_query(a string or an OR-list of titles) andlocations;countryselects the regional site.company_urlon LinkedIn pulls a whole company's jobs. - LinkedIn direct fetch: when you already hold the postings (a pasted link, the
job_urloff an earlier scrape), passlinkedin_job_urlsand/orlinkedin_job_ids, 1–100 per field; the two can be paired, and an id given both ways is fetched once. No search runs, so no other search input goes with them (nocountry, norows), and the result always carries job detail. The fetch's size is the number of distinct references you sent, so the poll'slimitis the only knob: 100 references exceed one page and need two polls (offset: 0, thenoffset: 50), billed per job exactly as in search mode, so 100 references cost at most 5.0 credits.len(result.jobs)can come back smaller than what you sent, and a dead or unfetchable posting costs nothing. - LinkedIn / Indeed seniority and industry filters: a title keyword alone pulls in the wrong employers ("CEO" matches charities, councils and staffing firms), so narrow with
experience_levels(internship,entry level,associate,mid-senior level,director,executive) andincluded_industries, on either board.excluded_industries,excluded_titlesandexcluded_companiesdrop what you name. Industry terms are LinkedIn industry names, such asRenewable Energy Power GenerationorUtilities, matched case-insensitively as a substring. Seniority and industry are labels a language model assigns from each job's detail page, so they can drift between runs. A job it could not label is kept, with aresult.warningsentry. Industry filters need the detail page, so they can't be combined withinclude_job_details: false. Filtered-out jobs are never returned or billed. On LinkedIn a filtered run reads more search pages, so it takes longer, and fewer jobs thanrowsdoes not mean the board ran out: a warning says the run stopped early and lists the labels it dropped most. On Indeed the pool is its first page plus whatever the second source adds (see Indeed depth), so a filter there thins that pool and cannot page deeper. - Indeed depth: Indeed's own first page is about 15 jobs;
startabove 0 returns no jobs. A second source tops a run up to about 50 jobs per location on a best-effort basis, and only when all of these hold: the country is the US, UK, Germany or France, there is nojob_typesfilter, Indeed's first page came back full (15 jobs), the run has at most 5 locations, and the second source's monthly request quota is not spent. Otherwise the run returns the first page, with awarningsentry when the second source failed or its quota ran out.listing_sourcesays which source answered (indeedorindeed+secondary). Fewer jobs thanrowsis normal, not a thin market, so don't re-run the same search. For more, run narrower searches (other titles or locations) and dedupe byjob_url; each location also spends the second source's quota. rowsandlimitare separate knobs, size both to what was asked.rowson the launch bounds what the scraper collects upstream (Indeed defaults to 25; LinkedIn to the upstream default, and takes 1-100: more is refused before the call);limiton the poll bounds what one page hands back (10). They are independent, sorows: 15with a default poll returns 10. For 15 roles sendrows: 15and poll withlimit: 15, one poll then delivers the ask and bills exactly it.arbeitsagentur_jobsandcareer_pagestake norows, so there the poll'slimitand stopping at the ask are the only guard.- Arbeitsagentur:
titleand/orlocation;radiusis in kilometres. - StepStone:
titleis required (there is no location-only search);locationis optional andradiusis in kilometres.rowsmust be 25, 50, 75 or 100 (whole 25-listing pages); page deeper by steppingstart_pagebyrows / 25, and stop at the first run that returns no jobs (a full page can come back a listing short, and an empty run costs nothing). Optional filters:posted_within_days(1 or 7),work_type,work_from_home,job_language,application_method,discipline. Salary is login-gated on StepStone, so salary fields are always empty.include_details: falsereturns the search-card teaser and no posting date. The run total istotal_foundrather thanjobs_total. - Each returned job carries title, location, employment type, salary (when available), the apply URL, and company fields, feed them straight into a sourcing or BD play.
- Post feeds:
usernameis the handle out of the LinkedIn URL, not the display name and not the full URL. - Post search: start from a keyword plus
author_title, read the page, and only then open a specific author's feed withprofile_postsif their history matters. Search finds the people; the feed reads one of them. - Post reactions: hand a feed row's
postUrltopost_reactionsto see who engaged; a reactor'sprofileUrlis the handle forprofile_postsor a profile enrich.
# 1. launch (free)
hyreflow tools execute linkedin_jobs \
--payload '{"titles_query":"recruiter","locations":["London"],"rows":15}'
# → { "request_id": "...", "status": "QUEUED" }
# 2. poll until COMPLETED, PARTIAL or FAILED (charged per job in result.jobs)
hyreflow tools execute hyreflow_native_get_linkedin_jobs \
--payload '{"request_id":"<id>","limit":15}'# direct fetch — postings you already hold, no search runs
hyreflow tools execute linkedin_jobs \
--payload '{"linkedin_job_urls":["https://www.linkedin.com/jobs/view/4123456789"],"linkedin_job_ids":["4000000001"]}'
# → { "request_id": "...", "status": "QUEUED" }
hyreflow tools execute hyreflow_native_get_linkedin_jobs \
--payload '{"request_id":"<id>","limit":2}'# executive search — renewable-energy CEOs in the UK, no charities or agencies
hyreflow tools execute linkedin_jobs \
--payload '{"titles_query":"CEO renewable","locations":["United Kingdom"],"rows":25,"experience_levels":["executive"],"included_industries":["Renewable Energy Power Generation","Utilities"]}'A job scrape is asynchronous: the launch returns a handle, not jobs. Keep polling the matching get_* until status is COMPLETED (or PARTIAL / FAILED); a still-running or empty result costs nothing. A post feed or post search has no launch half: the single call returns the posts. Email verification is asynchronous like a scrape.