# Upwork Jobs Scraper — Freelance Postings by Keyword (`accountable_eel/upwork-jobs-lookup`) Actor

Upwork jobs scraper by keyword. Run a search on Upwork's freelance job board and get one row per posting: title, link, description, hourly/fixed budget, experience level, skills, posted date. No login required. Pay per posting returned; empty searches are free.

- **URL**: https://apify.com/accountable\_eel/upwork-jobs-lookup.md
- **Developed by:** [Adrian Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.52 / 1,000 job posting returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Upwork Jobs Lookup: Upwork Jobs Scraper by Keyword

You write job searches the way you'd type them into Upwork's search box, and this actor runs
each one against Upwork's public job board and returns one clean row per posting: title, link,
description, hourly or fixed-price budget, experience level, skill tags, and how recently it was
posted. No login, no cookies, no Upwork account of any kind.

Upwork's search page sits behind a Cloudflare challenge for a plain request, but the page itself
is server-rendered with real job data once that challenge is cleared, so there's no need for a
headless browser here, only a proxy tier built for this kind of defense. See "Why this one"
below.

### Who it's for

If you build outbound lead lists, market intelligence tools, or agent workflows that need to know
what buyers are hiring freelancers for right now, this actor turns a keyword search into a flat
dataset instead of a page you'd otherwise scroll and copy by hand. Agencies scan it for
competitor demand signals ("who's paying for Shopify migrations this week"), freelancers use it
to build their own alert feed outside Upwork's own notification settings, and no-code builders
wire it into an n8n or Make flow that posts new postings into Slack the moment they match a
keyword. Pricing is pay-per-posting: a fraction of a cent for a row you actually got, and nothing
for a search that finds nothing.

This actor is one of this catalogue's search-shaped job lookups, alongside
`stepstone-jobs-lookup` (DACH region job boards) and `linkedin-jobs-search-lookup` (LinkedIn's
guest search). All three answer "who is hiring for this role, right now" from a keyword rather
than a company you already know, which is what separates them from the ATS lookups in the same
family (`greenhouse-jobs-lookup` and friends) that answer "what is this specific company
hiring for."

### Why this one

- **Reaches data a plain request can't.** A bare request to Upwork's search page returns an HTTP
  403 Cloudflare challenge page, confirmed live. This actor routes through a proxy tier built for
  exactly this kind of defense; the page itself, once reached, is ordinary server-rendered HTML
  (Upwork's frontend is a Nuxt app) with the job data already in it, no headless browser needed.
- **Real fields, not guessed ones.** Every column here was checked against a live captured page
  before being declared: hourly postings carry a rate range, fixed-price postings carry an "Est.
  budget" figure pulled from its own separate marker in the HTML (a different one than the hourly
  rate), and skill tags come from the same token list Upwork itself shows on the card.
- **Honest about what a guest can't see.** Proposal counts, client country, and client spend
  history only render on the logged-out job detail page, one extra request per posting, and only
  for some clients (a brand-new account with no spend history shows none of it at all). Fetching
  that page for every row would multiply the proxy cost per posting several times over, more than
  this actor's own asking price, so those three fields are not sold here. See the FAQ.
- **You are never billed for the same posting twice.** Every posting is deduplicated by its
  Upwork job ID across the pages of one search and, by default, across every search in the run.
- **Filters that don't lie about what they filter.** `jobType`, `minBudget`, `postedWithinDays`,
  `includeKeywords`, and `excludeKeywords` are all applied to the postings after they arrive,
  because Upwork's guest search exposes no confirmed server-side parameter for job type or
  budget. `category` is the same kind of honest compromise: it's appended to your search as extra
  keywords rather than sold as a hard filter Upwork doesn't actually expose to a logged-out
  request.

### What you get

One row per job posting by default (turn off "One row per job posting" in the Input tab to get
one row per search instead, with the whole posting list nested in `jobs`). Every row carries
these fields:

| Field | Type / format | Description |
| --- | --- | --- |
| `query` | text | The search line you passed in, unchanged. |
| `found` | boolean | `true` if the search returned at least one posting. `false` rows are never charged. |
| `status` | text | `OK`, `NOT_FOUND` (no postings for that search), `BAD_FORMAT` (a blank line), or `BLOCKED`. |
| `searchKeywords` | text | The keywords actually sent to Upwork, including any category text you added. |
| `jobCount` | number | How many postings this search returned after your filters. This is exactly what you're charged for. |
| `totalAvailable` | number | Upwork's own total-match count for the search, before this actor's page/depth limits. |
| `truncated` | boolean | `true` if more postings were available than you asked for, or if paging stopped early. |
| `jobs` | array | The full posting list. Present in every row; it's what gets expanded into separate rows in "one row per posting" mode. |
| `jobId` | text | Upwork's own numeric posting ID, stable, and what deduplication keys on. |
| `title` | text | Job title. |
| `url` | link | Permanent link to the posting on upwork.com. |
| `description` | text | The posting's own description, truncated to 500 characters (the search card itself is a truncated preview). |
| `jobType` | text | `hourly` or `fixed`. |
| `budgetText` | text | Budget exactly as Upwork prints it: an hourly rate range, or an "Est. budget" figure for fixed-price work. |
| `hourlyRange` | text | The hourly rate range on its own, only present on hourly postings. |
| `experienceLevel` | text | "Entry Level", "Intermediate", or "Expert", as Upwork classifies the posting. |
| `skills` | array | Skill tags Upwork lists on the card. |
| `postedAt` | text | How recently it was posted, in Upwork's own relative words ("Posted 3 weeks ago"). |
| `remote` | boolean | `true` if the job title itself says "remote". |
| `scrapedAt` | date (ISO) | When this actor fetched the row. |

A search that returns no postings comes back as a single `found: false` row with a
`status`/`message` explaining why, and is never charged. So does a blank line.

### Price

- **Job posting returned**: $2 per 1,000 job postings

Plus a $0.00005 start fee per run. Each event above is billed independently, only when it actually returns data — misses (`found:false`) are never charged.

You're charged **$2 per 1,000 postings returned**, not per search, plus a $0.00005 actor start
fee per run. A search that returns 12 postings costs twelve, a search that returns none costs
nothing, and a blank line costs nothing. The competing incumbent on the Apify Store charges
$3.50 per 1,000 and advertises "bypass CAPTCHAs" as its headline feature; this actor does the
same job for roughly 43% less.

Because you pay per posting, "Most postings to return per search" is your budget control. Leave
it at 30 and a ten-search run costs at most 300 postings' worth.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `queries` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~upwork-jobs-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"queries":["python developer"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

Paste one search per line:

```
python developer
wordpress plugin
data entry
```

**🔍 Search settings:**

| Input | What it does |
| --- | --- |
| `category` | Optional extra keywords appended to every search line (e.g. "Web Development"). Not a hard filter, see "Why this one" for why. |
| `maxJobsPerQuery` | Most postings to return per search. Default 30, capped at 50 (5 pages) by default. |

**🎯 Narrow the results,** applied here, to the postings after they arrive, since Upwork's guest
search exposes no confirmed server-side parameter for most of these. They combine with AND
across fields and OR within a field:

| Input | What it does |
| --- | --- |
| `jobType` | Keep only hourly or only fixed-price postings. |
| `minBudget` | Compares against the low end of the hourly range, or the fixed-price estimate. A posting whose budget text can't be parsed is kept, not dropped. |
| `postedWithinDays` | Keep only postings within this many days of now, based on Upwork's own relative timestamp. |
| `includeKeywords` | Keep only postings whose title or description contains one of these. |
| `excludeKeywords` | Drop postings whose title or description contains one of these. Applied after the include list. |
| `skipDuplicateJobs` | On by default. Each posting is returned, and billed, once per run even if two searches overlap. |

### Input

```json
{
  "queries": [
    "python developer"
  ]
}
```

One search per line, Upwork job search keywords, e.g. "python developer" or "wordpress plugin". Accepted formats: python developer, wordpress plugin, data entry.

### Sample output

| query | found | status | searchKeywords | jobCount | totalAvailable | truncated | jobs | jobId | title | url | description | jobType | budgetText | hourlyRange | experienceLevel | skills | postedAt | remote | isNew | firstSeenAt | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| python developer | true | OK | python developer | 10 | 72 | true | \<all postings found (full list)> | 2089438228379442832 | Python Developer for AI Prototype (LLM + State Comparison, Short Project) | https://www.upwork.com/jobs/span-class-highlight-Python-span-span-class-highlight-Developer-span-for-Prototype-LLM-State-Comparison-Short-Project\_~022089438228379442832/?referrer\_url\_path=/nx/search/jobs/ | I’m looking for a developer to help build a lightweight AI prototype using OpenAI or Anthropic APIs. This is NOT a full product build. This is a focused prototype to test a specific idea. Project Goal: Build a simple Python -based system that: Runs the same LLM task multiple times. Captures outputs and any intermediate state (memory/logs). Compares differences between runs. Classifies differences into simple categories: Stable Boundary Violation What This Means Think: •Run the same prompt 5–10 t | fixed | Est. budget: $100.00 |  | Intermediate | \["Python"] | Posted 3 weeks ago | false | \<is this posting new?> | <first seen on a run> | 2026-09-09T00:04:18.524Z |

A real row, captured live 2026-09-08 against the search "python":

| jobId | title | jobType | budgetText | experienceLevel | postedAt |
| --- | --- | --- | --- | --- | --- |
| 2090087423123075346 | Python Dev | hourly | Hourly: $3.00 - $3.00 | Entry Level | Posted 3 weeks ago |

A search that returns no postings comes back as a single `found: false` row, and is never
charged.

### Monitoring: only new results

Turn on **Only return results that are new since the last run** and this actor becomes a job monitor. Every posting Upwork returns is checked against the job IDs your previous run already delivered, and anything you have seen is dropped before you are billed. On a marketplace where the first hour matters, that is the difference between a feed and an alert. A run with nothing new returns no rows and costs only the run fee, and each run logs a count like `3 new of 50 fetched`.

The first run has nothing to compare against, so it returns everything it finds and remembers it. From the second run on you get only what Upwork has posted since.

To turn that into an alert:

1. Save your query as a task with the checkbox on.
2. Add a **Schedule** to that task, every 15 minutes or hourly.
3. Add a webhook on **Run succeeded**, pointing at Slack, n8n, Make or Zapier.

Memory lives in a named key-value store, `upwork-jobs-lookup-delta`, one record per query, holding the last 5,000 postings per watchlist. Leave the checkbox off but fill in **Watchlist name** and every posting comes back stamped with `isNew` and `firstSeenAt`.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~upwork-jobs-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"queries":["python developer"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~upwork-jobs-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"queries":["python developer"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~upwork-jobs-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"queries":["{{search}}"]}`, mapping the row's search into the `queries` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Upwork Jobs Scraper | Apify" — the agent will find and run this actor.

### Tips

- Run a small "Most postings to return per search" value (10 to 20) on your first try with a new
  keyword to sanity-check the match quality before spending on a bigger pull.
- Combine `includeKeywords` and `excludeKeywords` to cut noise from an ambiguous keyword. A search
  for "python" alone returns tutoring gigs, data entry work mislabeled with the word, and real
  development roles all mixed together; excluding "tutor" and "lesson" trims a lot of it.
- `minBudget` reads the low end of an hourly range, so a $3 to $50 hourly posting passes a
  `minBudget` of 10 even though the top of its range is well above your floor. Treat it as "could
  pay at least this much", not "will pay at least this much".
- A `BLOCKED` status means Upwork's challenge caught this run. Lowering "Max concurrency" and
  trying again shortly usually clears it, since UNBLOCKER sessions rotate.
- Set up a scheduled Task with `postedWithinDays` set to match your run cadence (e.g. 1 for a
  daily run) so each run only bills for postings you haven't already seen in a prior run's window.

### vs. alternatives

| | What it costs | What you get | Trade-off |
| --- | --- | --- | --- |
| **This actor** (`upwork-jobs-lookup`) | $0.002 per posting returned, $0.00005 actor start, nothing for a search with no matches | One row per posting: title, link, description, hourly/fixed budget, experience level, skills, posted date, filtered by job type, budget, and keywords | No proposal count, client country, or client spend history, those only exist on a per-posting detail page and fetching one per row would cost more than this actor charges. Use Upwork's own site if you need that for a specific posting. |
| **neatrat/upwork-job-scraper** | $3.50 per 1,000 postings | The current Store leader for Upwork search, similar posting fields, advertises CAPTCHA bypass as its headline feature | Same job, roughly 75% more expensive per posting than this actor. |
| **Upwork's own search** | Free | The same data, one search at a time, in a browser | It's the same source. This actor exists for the batch: many searches in, one flat table out, deduplicated and filtered, callable from a script or a schedule instead of a browser tab. |
| Doing it yourself | Your time, plus a Cloudflare-grade proxy budget, HTML that changes without notice, and building your own dedup and filter logic | The same data | The proxy wiring, the field mapping for both pricing shapes (hourly vs. fixed), and the retry/dedup handling are the maintenance burden this actor absorbs. |

Prices for third-party tools are their published list prices as of September 2026 and are not
tracked here, check the vendor before relying on the comparison.

### FAQ

**Why is a row empty, or why does `found` say `false`?**
Either the input line is blank (`status: BAD_FORMAT`), Upwork's challenge blocked the request
(`status: BLOCKED`), or the search genuinely returned no postings (`status: NOT_FOUND`). Check
the `message` column for the specific reason. None of these are billed.

**Am I charged for a miss?**
No. Billing only fires for postings actually returned in `jobs`. A blocked search, a blank line,
or a search with zero matches all produce a row and none of them cost anything.

**Why don't you include proposal count, client country, or client spend?**
Those fields only render on a posting's own detail page, one extra request per posting through
the same paid proxy this actor already uses, and even there they're inconsistent (a brand-new
client account with no spend history shows none of it). Fetching a second page per row would
multiply the proxy cost several times over per 1,000 postings, more than this actor's own
asking price, so those fields are not sourced or faked here.

**Is this a live search or a stored database?**
Live. Every run searches Upwork's current listings, there's no cached snapshot behind it. A
posting that's filled or removed since the last run simply won't appear again.

**Does this handle rate limits or blocking?**
Each request goes through Apify's UNBLOCKER proxy group. A run that gets `BLOCKED` mid-search
still returns whatever postings it already collected before the block, and a search that starts
blocked returns a `BLOCKED` row so you know to retry rather than assume there were no postings.

**Do I need to configure proxies?**
No. UNBLOCKER is set as the default proxy group for this actor, since a plain request or the
default datacenter proxy gets a Cloudflare challenge page instead of results.

**Is this GDPR-relevant?**
The postings are public job listings. No personal data about the client or freelancers on either
side is collected; client identity fields (country, spend, proposal count) are the exact fields
this actor deliberately doesn't source, see the question above.

**Can I schedule this to re-check the same keywords on a cadence?**
Yes, set up an Apify Task with a schedule (hourly, daily, weekly) and point it at this actor with
a saved input. Pair it with `postedWithinDays` so each run only bills for postings freshly posted
since your last check.

**Can an AI agent call this directly?**
Yes. It's registered on the Apify MCP server, an agent in Claude, Cursor, or another MCP client
can find and run it by name ("Upwork Jobs Scraper | Apify"), or you can call the REST endpoint
shown above from any script or workflow tool.

### Related actors

- [StepStone Jobs Search Lookup](https://apify.com/accountable_eel/stepstone-jobs-lookup), the
  same keyword-search shape for the DACH region's largest job board instead of Upwork.
- [LinkedIn Jobs Search Lookup](https://apify.com/accountable_eel/linkedin-jobs-search-lookup),
  the same keyword-search shape against LinkedIn's own guest job search.
- [Greenhouse Jobs Lookup](https://apify.com/accountable_eel/greenhouse-jobs-lookup), for when you
  already know the company and want every posting on its own careers page instead of a
  marketplace-wide keyword search.

# Actor input Schema

## `queries` (type: `array`):

One search per line, Upwork job search keywords, e.g. "python developer" or "wordpress plugin". Accepted formats: python developer, wordpress plugin, data entry. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Keep only postings whose title or description contains at least one of these words. Case doesn't matter and partial words work. Leave empty to keep every posting.

## `excludeKeywords` (type: `array`):

Optional. Drop any posting whose title or description contains one of these words. Applied after the include list above.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `category` (type: `string`):

Optional. Upwork's logged-out search has no separate category filter, so this is appended to every search line as extra keywords (e.g. "Web Development") rather than a hard filter. Leave empty to search exactly what you typed above.

## `maxJobsPerQuery` (type: `integer`):

Upwork shows 10 postings per page. This actor stops at 5 pages (50 postings) per search by default to keep UNBLOCKER proxy cost predictable, raise it if you need more. You pay per posting returned, so this is also your budget control.

## `jobType` (type: `string`):

Keep only hourly or only fixed-price postings. Applied after the results arrive.

## `minBudget` (type: `integer`):

Optional. For hourly postings this compares against the low end of the posted rate range; for fixed-price postings, the posted estimated budget. A posting whose budget text can't be parsed is kept, not dropped, you're never silently denied a real result.

## `postedWithinDays` (type: `integer`):

Optional. Keep only postings Upwork's own relative timestamp ("Posted 3 weeks ago") places within this many days. Leave at 0 to keep every posting.

## `skipDuplicateJobs` (type: `boolean`):

On by default. Overlapping searches routinely surface the same posting, with this on, each posting is returned, and billed, exactly once per run.

## `deltaMode` (type: `boolean`):

Turns this actor into a monitor. A posting counts as new when Upwork's own job ID for it has not been returned by a previous run of the same watchlist. Already-seen postings are dropped before you are billed, so a run with nothing new costs only the run fee. The first run has nothing to compare against, so it returns everything and remembers it; from the second run on you get only what Upwork has posted since.

## `deltaKey` (type: `string`):

Leave empty and we derive one from this run's settings, so two schedules with different settings keep separate memories — and changing a setting starts a fresh watchlist that re-baselines once. Type your own name to keep one memory across such a change, or to have two schedules share one. Naming a watchlist with the checkbox above off returns every result but still stamps each one with 'Is this new?' and 'First seen', so you can filter them yourself in Sheets, n8n or Make.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each job posting found gets its own row instead of being grouped under its search. You're still only charged once per search, no matter how many rows it produces.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

## `proxyConfiguration` (type: `object`):

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "queries": [
    "python developer"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "category": "",
  "maxJobsPerQuery": 30,
  "jobType": "any",
  "minBudget": 0,
  "postedWithinDays": 0,
  "skipDuplicateJobs": true,
  "deltaMode": false,
  "deltaKey": "",
  "columns": [
    "searchKeywords",
    "jobCount",
    "totalAvailable",
    "truncated",
    "jobs",
    "jobId",
    "title",
    "url",
    "description",
    "jobType",
    "budgetText",
    "hourlyRange",
    "experienceLevel",
    "skills",
    "postedAt",
    "remote",
    "isNew",
    "firstSeenAt"
  ],
  "expandRows": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "python developer"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/upwork-jobs-lookup").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": ["python developer"],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/upwork-jobs-lookup").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "python developer"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/upwork-jobs-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/upwork-jobs-lookup"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/3bPWbW9FIagtdNX1z/builds/RdSz54f3pMN3zVmkg/openapi.json
