Workana Projects & Client Demand Scraper (LATAM)
Pricing
Pay per event
Workana Projects & Client Demand Scraper (LATAM)
Scrape public project listings from Workana, Latin America's largest freelance marketplace: budget, skills, client country, proposal count and client reputation signals. Only filters that Workana actually applies. Declares its own coverage ceiling. No login, no session cookies.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Oswaldo Carabano
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 hours ago
Last modified
Categories
Share
Scrape public project listings from Workana, Latin America's largest freelance marketplace. Budgets parsed into numbers, client country, proposal counts and client reputation signals — plus a coverage report that tells you what the run could not reach.
No login. No session cookies. Nothing that expires.
Two things this actor does that the alternatives do not
1. Every filter it offers actually filters
Workana accepts a lot of URL parameters with HTTP 200 and then ignores them. We
tested each one and found thirteen that look like filters and do nothing:
budget_min, budget, is_hourly, project_type, duration, published, date,
urgent, payment_verified, max_bids, featured, hourly, budget_from.
Offering any of those would promise a filter that does not exist — you would pay for rows you did not ask for. So this actor offers only the five that Workana really applies: category, subcategory, skill, country and free-text search.
language is not offered either, and that one is worth explaining: it does not
narrow results, it widens them. Measured on the live site:
| Query | Projects declared |
|---|---|
country=BR | 2,826 |
country=BR + language=pt | 10,000 (Workana's cap) |
2. It tells you what it could not reach
Workana serves 7 projects per page and stops at page 50. That is a limit of the site, not of any scraper: about 350 projects per query is the ceiling for everyone. Its own result counter is also capped at 10,000, so for large queries even Workana will not tell you how much there is.
This actor does two things about it:
- Splits large queries automatically along the country axis, which we measured to be almost purely additive (4% overlap, 6.8× more unique projects across 6 countries).
- Writes a coverage report — one entry per query, in the key-value store under
COVERAGE— with what was declared, what was delivered, whether the page-50 ceiling was hit, and how many projects are out of reach. If a run cannot give you everything, you will know from the output, not by guessing.
Output
One row per project in the default dataset, 54 fields. The coverage report and
any failed requests go to the key-value store, under COVERAGE and ERRORS, so no
export ever mixes row types.
If a run has no failures, no ERRORS record is created at all — an empty error file
would suggest something went wrong when nothing did.
{"slug": "integracion-de-crm-con-meta-para-atribucion-de-conversiones","url": "https://www.workana.com/es/job/integracion-de-crm-con-meta-...","title": "Integración de CRM con Meta para Atribución de Conversiones","budget_raw": "USD 100 - 250","budget_min": 100,"budget_max": 250,"budget_currency": "USD","budget_unit": "project","budget_type": "range","is_hourly": false,"client_name_masked": "Plaga D.","client_country_code": "AR","client_country_name": "Argentina","client_rating": 5,"client_has_rating": true,"client_payment_verified": true,"total_bids": 30,"total_bids_raw": "Propuestas: 30","posted_at": "2026-08-18T19:30:31.906Z","posted_at_raw": "Hace 14 horas","posted_age_hours": 14,"skills": ["API", "CRM", "Facebook", "PHP", "Python", "JavaScript"],"skill_slugs": ["api", "crm", "facebook", "php", "python", "javascript"],"category": "it-programming","category_name": "Programación y Tecnología","subcategory": "web-development","subcategory_name": "Programación Web","project_scope": "Crear un nuevo sitio personalizado","project_size": "Medio","from_cache": false,"data_age_hours": 0}
The posting form's answers, extracted
Workana appends the client's answers to its posting form at the end of every description — and they are structured data hiding inside prose. This actor pulls them out into their own fields instead of leaving you to parse them:
| Field | Present in |
|---|---|
category, category_name | 100% |
subcategory, subcategory_name | 100% |
project_scope — "Create a new custom site", "Small bug or change"… | 68% |
project_size — Small / Medium / Large | 30.5% |
client_needs, engagement_type, required_availability, time_required, client_currently_has, required_platforms, word_count_raw | varies by subcategory |
extra_attributes | the subcategory-specific tail, with its original label |
Two things worth knowing:
categoryis always filled, even when you query by subcategory or by skill. The parent-child relationship comes from Workana's own taxonomy, not from a guess.extra_attributesis by design, not a gap. Workana asks questions specific to each subcategory — "Logo type", "App type", "Payment processor" — and that tail is endless. The cross-cutting questions get their own columns; the rest travels inextra_attributeswith its original label, so nothing is lost and the schema does not grow columns that are almost always empty.description_textno longer repeats that block. The metadata comes out as fields, so leaving it in the text too would just inflate the largest field in the dataset.description_htmlkeeps Workana's original, untouched.
Every parsed field keeps its original
budget_min: 100 comes with budget_raw: "USD 100 - 250". total_bids: 30 comes
with total_bids_raw: "Propuestas: 30". The parsed number is our interpretation; the
raw string is Workana's data, and you can always check our work.
This matters more than it sounds. Workana writes its budgets in the language you
request, and the thousands separator changes with it: "USD 1,000" in English is
one thousand, "USD 1.000" in Spanish is also one thousand. A parser that ignores
the language turns one of them into 1 — a three-orders-of-magnitude error in the
field people filter on most. Every row carries its locale so you can verify.
budget_type — why a missing number is not a missing value
A quarter of Workana budgets have no lower bound at all: the client posted
"Less than USD 50", not a range. Reporting budget_min: null there and calling it
a coverage gap would be wrong — there is no minimum to report. And some projects
carry no figure whatsoever ("Open"), which is a different thing again.
So every row says which case it is:
budget_type | Meaning | min | max | Measured (n=272) |
|---|---|---|---|---|
range | "USD 100 - 250" | ✅ | ✅ | 70.6% |
max_only | "Less than USD 50" — no lower bound exists | null | ✅ | 25.0% |
min_only | "Over USD 3,000" — no upper bound exists | ✅ | null | 4.4% |
fixed | a single figure | ✅ | ✅ (same) | rare |
unspecified | Workana shows no figure at all ("Open") | null | null | rare |
null | the listing did not carry the field | null | null | not seen |
Sort or filter on budget_type before you compare numbers. Averaging budget_min
across a mixed set silently drops the quarter of projects that never had one, which
biases the average upward. And unit matters too: some budgets are
"USD 15 - 45 / hour"budget_unit is hour there, not project.
The distinction is decided by structure, not vocabulary: a budget string with no
digits is unspecified in any language. We only ever observed the English wording
("Open"), so guessing at Spanish and Portuguese labels would have been inventing
data we had not measured.
Field coverage — measured, not estimated
Measured on 718 projects across 12 subcategories and 8 countries. We publish how often a field carries useful information, not how often the key exists:
| Field | Useful in |
|---|---|
slug, title, description, budget_raw, budget_type, client_country_*, total_bids, posted_at, client_name_masked | 100% |
budget_max | 95.6% |
budget_min | 75.0% — and that is not a gap, see below |
skills, skill_slugs | 98.6% |
last_client_message_raw | 51.7% |
client_rating | 19.4% — see below |
client_payment_verified (true) | 18.9% |
is_hourly (true) | 11.7% |
deadline_days | 1.9% |
client_plan | 0.4% |
⚠️ About client_rating
Workana returns a rating for every project, but 80.5% of them are 0.00, which
means "no ratings yet" — not "rated zero". That is why every row also carries
client_has_rating. If you filter on rating without checking that flag you will
discard four fifths of the market believing you discarded bad clients, when you
discarded clients with no history.
We could have published "client rating: 100% coverage". It would have been true and misleading.
Fields we do not return
isUrgent, isCompany and isInvite were false in all 718 projects — an
anonymous visitor never sees them set. A column with one constant value is not data,
so they are not in the output. shortDescription was empty in all 718.
Input
| Option | Notes |
|---|---|
category | One of the 8 top-level categories. Leave empty to sweep all of them |
subcategory | e.g. web-development, mobile-development, logo-design |
skill | e.g. python, react-js, laravel. One skill applies — Workana keeps the first value and silently drops the rest |
country | Client country, ISO-2: AR, BR, MX, ES, CO, PE… |
query | Free-text search. The only axis without a fixed vocabulary |
maxItems | Hard cap. Default 500 |
locale | es, en or pt — changes the language of Workana's own text |
maxCacheAgeHours | Default 6. Set to 0 to force fresh data |
maxConcurrency | Default 3, and that number is measured (see below) |
Slugs are checked before the run starts
A wrong skill slug returns zero projects with HTTP 200 on Workana: react gives
nothing, react-js gives hundreds. That is the worst kind of failure — an empty run
that looks like a correct one. This actor checks the slug first and tells you what is
wrong instead of charging you for nothing.
Freshness
Proposal counts move fast. Measured across 401 projects: the median goes from about
1 proposal in the first hours to about 10 by the twelfth. If you are using
total_bids to judge competition, a stale row is a wrong row.
So the default cache age is 6 hours, and every row declares from_cache,
fetched_at and data_age_hours. Set maxCacheAgeHours: 0 for live data. You
always know how old a row is.
Speed and politeness
maxConcurrency defaults to 3, and that is a measurement: 180 requests at
concurrency 1, 2 and 3 completed with zero failures, while concurrency 5 produced
5% HTTP 503 responses from Workana's own servers. Raising it makes runs less
reliable, not faster — which is why the maximum is 5 and not 20.
Billing
Pay per event, and two rules:
- Failed requests are never charged. If a request fails, you get an entry under
ERRORSin the key-value store explaining what happened, free. - Charges happen right after a row is delivered, never in a batch at the end. If a run is aborted halfway you pay for what you received and nothing more.
What this actor does not do
- It does not log in, use session cookies, or need credentials.
- It does not solve captchas.
- It does not scrape freelancer profiles. Project listings arrive already anonymised by Workana; a freelancer profile is an identified person with their rate and history. That is a different product with a different risk, and it is not in here.
- It does not promise a coverage percentage. Workana's own result counter is capped, so the true size of the market is unknown — and a percentage of an unknown is a made-up number. You get what was delivered, what was declared, and where the ceiling was hit.