# Dataset Filter & Transform (`nerolabs/dataset-filter-transform`) Actor

Returns rows of any Apify dataset, CSV/Excel/JSON file URL or Google Sheet that pass filter rules (25 operators, AND/OR, date ranges that fix a scraper's ignored date filter) after transforms (rename, cast, compute, regex), then sort, dedupe, export, webhook. Agent-ready: pay per event (x402, MCP).

- **URL**: https://apify.com/nerolabs/dataset-filter-transform.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** Developer tools, Automation, Agents
- **Features:** Limited permissions
- **Stats:** 198 total users, 62 monthly users, 92.6% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 kept rows

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

**Stop writing one-off scripts to clean up scraper output.** Dataset Filter & Transform takes any Apify dataset, a CSV, Excel or JSON file by URL, a Google Sheet, or a JSON array you paste in, and lets you **filter rows by rule** and **transform fields**: rename, drop, trim, change case, cast types, compute a field from arithmetic, extract text with a regex, replace, split, fix dates, map values, hash a stable id, or template a new field from existing ones. Then sort, dedupe, limit, and get back exactly the rows and fields you wanted, as a dataset, a ready-to-open CSV or Excel file, a named dataset that accumulates across runs, or a POST to your webhook. All from a JSON config, no code, no spreadsheet formulas to babysit.

### Why use Dataset Filter & Transform?

Every scraper dumps more than you need: rows you don't want, messy strings, numbers stored as `"$1,234.50"` text, dates in five formats, fields with the wrong name for your CRM import. The usual fix is a throwaway Python script or a pile of Excel formulas, redone every time. This Actor turns that into a reusable, scheduled, API-callable step:

- **Sales and lead gen**: keep only leads in your target country with a phone number, trim and lowercase emails, extract the company domain, dedupe by email, compute a lead score.
- **E-commerce**: keep only in-stock products under a price threshold, cast price strings to real numbers, compute a margin field, sort by price, take the top 100.
- **Job boards and listings**: keep only rows posted in the last 7 days, normalise the posted date, drop the fields you never use.
- **Data pipelines**: chain this after any scraper (via Apify's dataset-to-dataset integrations or a schedule) to always hand the next step clean, filtered data, appended into one named dataset.
- **CRM prep**: rename scraped field names to match your import template, map country codes to full names, export to CSV.
- **Files that never touched Apify**: point it at a CSV export from your CRM, an Excel report, or a Google Sheet and clean it the same way.

Because it runs on Apify, you get scheduling, API access, dataset integrations (Zapier, Make, Google Sheets, webhooks), and full run history for free.

### How to use Dataset Filter & Transform

1. Click **Try for free** (or **Start**) on this Actor.
2. Bring in your rows: pick an existing dataset in **Dataset to process**, paste a link into **File URL** (CSV, TSV, Excel, JSON, JSON Lines, or a Google Sheet), or paste rows into **Data (inline)**.
3. Add **Transform steps**, a JSON list of operations applied in order (see below).
4. Add **Filter conditions**, rows are kept only if they pass, using AND or OR.
5. Optionally **sort**, keep one row per **distinct** value, and set a **limit**.
6. Pick where the result goes: this run's dataset (always), a **named dataset** that accumulates across runs, a **CSV or Excel file**, or a **webhook**. Run.

The default input runs in a couple of seconds against a small built-in example so you can see exactly how it behaves before pointing it at your own data.

### Bringing in a file or a Google Sheet

Set **File URL** to any public link. The format is detected from the extension, the content type and the content itself, or force it with **File format**:

- **CSV / TSV**: header row required; quotes, embedded newlines and semicolon or tab delimiters are handled.
- **Excel (.xlsx)**: the first sheet, header row in row 1; dates come out as ISO strings, formulas as their computed values.
- **JSON**: an array, an object wrapping an array (`{"items": [...]}`, `{"data": [...]}`), or one object per line (JSON Lines).
- **Google Sheets**: paste the normal sheet link. Set sharing to "Anyone with the link can view" (or File > Share > Publish to the web); the Actor converts it to the CSV export link for you, including the specific tab if the link carries a `gid`.

Up to 100 MB per run. Downloading the file is never charged, only the rows you keep are.

### Transform steps

Each step is one JSON object; steps run top to bottom, so a later step can use an earlier step's output (e.g. trim a field, then reference it in a template). Every step writes a top-level field.

**Nested values (dotted paths):** filter conditions, **Sort by**, **Keep one row per distinct value of** and these steps can read a nested value with a dotted path like `address.city`: `copy`, `addField` placeholders, `compute` expressions, `default`, `coalesce`, `replace`, `split`, `joinArray`, `substring`, `round`, `dateFormat`, `dateDiff`, `extractDomain`, `mapValues`, `hash`, `parseJson` and `length`. The other steps, `rename`, `drop`, `keep`, `cast`, `trim`, `uppercase`, `lowercase` and `regexExtract`, only work on top-level fields, so `copy` a nested value to a top-level field first: `{"op":"copy","from":"address.city","to":"city"}`, then `{"op":"trim","field":"city"}`. Without `into`, a step that reads `address.city` puts its result in a new top-level field literally named `address.city`; the nested value itself is never changed.

| `op` | What it does | Example |
|---|---|---|
| `rename` | Rename a field (top-level only) | `{"op":"rename","from":"e-mail","to":"email"}` |
| `drop` | Remove fields (top-level only) | `{"op":"drop","fields":["internal_id"]}` |
| `keep` | Keep only listed fields, drop the rest (top-level only) | `{"op":"keep","fields":["name","email"]}` |
| `copy` | Copy a value (nested paths allowed) into a new field | `{"op":"copy","from":"address.city","to":"city"}` |
| `setField` | Set a field to a fixed value of any JSON type | `{"op":"setField","field":"source","value":"apify"}` |
| `default` | Fill a field only when it's blank | `{"op":"default","field":"country","value":"US"}` |
| `trim` / `uppercase` / `lowercase` | String whitespace and case (top-level only) | `{"op":"trim","field":"name"}` |
| `cast` | Convert to `number`, `string`, or `boolean` (top-level only) | `{"op":"cast","field":"price","to":"number"}` |
| `addField` | Build a new field from a template | `{"op":"addField","field":"fullName","template":"{{first}} {{last}}"}` |
| `compute` | Arithmetic over numeric fields | `{"op":"compute","field":"total","expression":"price * qty","round":2}` |
| `round` | Round a number | `{"op":"round","field":"price","decimals":2}` |
| `replace` | Replace text (all occurrences), plain or regex | `{"op":"replace","field":"phone","search":"[^0-9+]","replacement":"","regex":true}` |
| `regexExtract` | Pull text out with a regex (top-level only) | `{"op":"regexExtract","field":"sku","pattern":"ITEM-(\\d+)-","into":"itemNumber"}` |
| `split` | Split on a separator into an array, or take one part | `{"op":"split","field":"name","separator":" ","index":-1,"into":"lastName"}` |
| `joinArray` | Join an array into one string | `{"op":"joinArray","field":"tags","separator":", "}` |
| `substring` | Slice a string (negative start counts from the end) | `{"op":"substring","field":"postcode","start":0,"length":4,"into":"area"}` |
| `coalesce` | First non-blank of several fields | `{"op":"coalesce","fields":["mobile","phone","landline"],"into":"bestPhone"}` |
| `dateFormat` | Normalise a date: `iso`, `date` (YYYY-MM-DD), `unix`, `unixMs`, or a pattern like `DD/MM/YYYY` | `{"op":"dateFormat","field":"posted","format":"date"}` |
| `dateDiff` | Days (or hours, minutes, seconds) between two dates, or since a date until now | `{"op":"dateDiff","from":"posted","into":"daysAgo"}` |
| `extractDomain` | Domain from an email address or URL | `{"op":"extractDomain","field":"email","into":"company_domain"}` |
| `mapValues` | Look values up in a table (case-insensitive), with an optional default | `{"op":"mapValues","field":"country","map":{"US":"United States","GB":"United Kingdom"},"default":"Other"}` |
| `hash` | A stable id from one or more fields (sha256, sha1 or md5), for dedupe and joins | `{"op":"hash","fields":["email","country"],"into":"rowId"}` |
| `parseJson` | Turn a JSON string field into a real object | `{"op":"parseJson","field":"attributes"}` |
| `length` | Length of a string or array | `{"op":"length","field":"tags","into":"tagCount"}` |

Number parsing is lenient by default: `"$1,234.50"`, `"49 USD"` and `"(300)"` (accounting negative) all read as real numbers for `cast`, `compute`, `round`, sorting and numeric filters. Dates are read from ISO strings, Unix timestamps (seconds or milliseconds), spreadsheet-style text like `March 14, 2026`, and day-first `14/03/2026`; ambiguous slash dates like `03/04/2026` are read month-first, the way a US spreadsheet would.

### Filter conditions

Each condition is `{"field": "...", "operator": "...", "value": ...}`. Combine every condition with **AND** (must match all) or **OR** (match any).

| Group | Operators |
|---|---|
| Text | `equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `endsWith`, `matchesRegex`, `in`, `notIn` (value = array) |
| Numbers | `greaterThan`, `lessThan`, `greaterOrEqual`, `lessOrEqual`, `between` (value = `[min, max]`) |
| Presence | `isEmpty`, `isNotEmpty`, `isTrue`, `isFalse` (reads yes/no, 1/0, true/false) |
| Lists | `arrayContains`, `lengthGreaterThan`, `lengthLessThan` |
| Dates | `dateAfter`, `dateBefore` (value = a date, `"today"` or `"now"`), `withinLastDays`, `olderThanDays` (value = number of days) |

String comparisons are case-insensitive by default (`"US"` matches `"us"`); turn on **Case-sensitive filters by default**, or set `"caseSensitive": true` on one condition, to match letter case exactly. Case-sensitive is not byte-for-byte: surrounding spaces are still trimmed and repeated spaces collapsed, and `equals`, `notEquals`, `in`, `notIn` and `arrayContains` still compare two values that both read as numbers as numbers (`"1,200"` equals `1200` while **Lenient numbers** is on). `matchesRegex` ignores the global switch: it is case-insensitive unless that condition itself sets `"caseSensitive": true`.

### Sort, dedupe and limit

Applied after filtering, in this order:

- **Sort by**: `[{"field": "price", "direction": "desc"}]` or the shorthand `["-price", "name"]`. Numbers sort numerically, text case-insensitively, blanks last.
- **Keep one row per distinct value of**: field names, e.g. `["email"]`. Only the first kept row per distinct value survives, so *sort by date descending, then distinct by email* keeps the newest row per email.
- **Maximum rows to output** and **Skip the first N rows**: `top 100 by price`, or paging.

Rows removed by sorting, deduping or the limit are never charged.

### Ready-made setups for popular scrapers

Copy one of these into the **JSON** input tab, put your scraper run's dataset ID in `datasetId`, and run. Each one was tested on Apify with rows shaped like that scraper's real output. Change the numbers to suit you.

#### Google Maps Scraper: call list of businesses with no website

From a [Google Maps Scraper](https://apify.com/compass/crawler-google-places) run, keep businesses that have a phone number but no website and at least 5 reviews, clean the phone numbers, remove repeats of the same number, busiest first, as a CSV.

```json
{
  "datasetId": "YOUR_GOOGLE_MAPS_DATASET_ID",
  "transforms": [
    {
      "op": "replace",
      "field": "phone",
      "search": "[^0-9+]",
      "replacement": "",
      "regex": true
    },
    {
      "op": "keep",
      "fields": [
        "title",
        "categoryName",
        "phone",
        "address",
        "city",
        "totalScore",
        "reviewsCount",
        "website",
        "url"
      ]
    }
  ],
  "filters": [
    {
      "field": "phone",
      "operator": "isNotEmpty"
    },
    {
      "field": "website",
      "operator": "isEmpty"
    },
    {
      "field": "reviewsCount",
      "operator": "greaterOrEqual",
      "value": 5
    }
  ],
  "filterCombineMode": "AND",
  "sortBy": [
    "-reviewsCount"
  ],
  "distinctBy": [
    "phone"
  ],
  "exportFormats": [
    "csv"
  ]
}
```

#### Instagram Scraper: top posts of the last 30 days

From an [Instagram Scraper](https://apify.com/apify/instagram-scraper) run, add an `engagement` score (likes plus comments), keep posts from the last 30 days, and return the top 50.

```json
{
  "datasetId": "YOUR_INSTAGRAM_DATASET_ID",
  "transforms": [
    {
      "op": "compute",
      "field": "engagement",
      "expression": "likesCount + commentsCount"
    },
    {
      "op": "dateFormat",
      "field": "timestamp",
      "format": "date"
    },
    {
      "op": "keep",
      "fields": [
        "ownerUsername",
        "url",
        "caption",
        "likesCount",
        "commentsCount",
        "engagement",
        "timestamp"
      ]
    }
  ],
  "filters": [
    {
      "field": "timestamp",
      "operator": "withinLastDays",
      "value": 30
    }
  ],
  "sortBy": [
    "-engagement"
  ],
  "limit": 50,
  "exportFormats": [
    "csv"
  ]
}
```

#### TikTok Scraper: videos with the best engagement rate

From a [TikTok Scraper](https://apify.com/clockworks/tiktok-scraper) run, pull the creator name and follower count out of `authorMeta`, work out the engagement rate (likes, comments and shares as a percent of plays), drop videos under 1,000 plays, and return the top 100.

```json
{
  "datasetId": "YOUR_TIKTOK_DATASET_ID",
  "transforms": [
    {
      "op": "copy",
      "from": "authorMeta.name",
      "to": "author"
    },
    {
      "op": "copy",
      "from": "authorMeta.fans",
      "to": "followers"
    },
    {
      "op": "compute",
      "field": "engagementRate",
      "expression": "(diggCount + commentCount + shareCount) / playCount * 100",
      "round": 2
    },
    {
      "op": "keep",
      "fields": [
        "author",
        "followers",
        "text",
        "playCount",
        "diggCount",
        "commentCount",
        "shareCount",
        "engagementRate",
        "createTimeISO",
        "webVideoUrl"
      ]
    }
  ],
  "filters": [
    {
      "field": "playCount",
      "operator": "greaterOrEqual",
      "value": 1000
    }
  ],
  "sortBy": [
    "-engagementRate"
  ],
  "limit": 100,
  "exportFormats": [
    "csv"
  ]
}
```

#### LinkedIn Jobs Scraper: this week's jobs with the company domain

From a [LinkedIn Jobs Scraper](https://apify.com/curious_coder/linkedin-jobs-scraper) run, keep jobs posted in the last 7 days, add the company's domain (ready for an email finder or CRM), remove repeats, newest first.

```json
{
  "datasetId": "YOUR_LINKEDIN_JOBS_DATASET_ID",
  "transforms": [
    {
      "op": "dateFormat",
      "field": "postedAt",
      "format": "date"
    },
    {
      "op": "extractDomain",
      "field": "companyWebsite",
      "into": "companyDomain"
    },
    {
      "op": "keep",
      "fields": [
        "title",
        "companyName",
        "companyDomain",
        "location",
        "postedAt",
        "applicantsCount",
        "link"
      ]
    }
  ],
  "filters": [
    {
      "field": "postedAt",
      "operator": "withinLastDays",
      "value": 7
    }
  ],
  "sortBy": [
    "-postedAt"
  ],
  "distinctBy": [
    "link"
  ],
  "exportFormats": [
    "csv"
  ]
}
```

To run one of these automatically after every scraper run, add this Actor as an integration on the scraper (its **Integrations** tab, run when the run succeeds) and set `"datasetId": "{{resource.defaultDatasetId}}"` so each new run's results are cleaned as soon as they land.

### Input

See the **Input** tab for the full schema. The three ways to bring in data:

- **`datasetId`**: point at any existing Apify dataset (yours or from another Actor's run).
- **`fileUrl`**: a CSV, TSV, Excel, JSON or JSON Lines file, or a Google Sheet link.
- **`data`**: paste a JSON array directly for quick, ad-hoc jobs.

```json
{
  "fileUrl": "https://docs.google.com/spreadsheets/d/1AbC.../edit#gid=0",
  "transforms": [
    { "op": "trim", "field": "name" },
    { "op": "lowercase", "field": "email" },
    { "op": "cast", "field": "revenue", "to": "number" },
    { "op": "dateFormat", "field": "signupDate", "format": "date" },
    { "op": "extractDomain", "field": "email", "into": "domain" }
  ],
  "filters": [
    { "field": "country", "operator": "equals", "value": "US" },
    { "field": "revenue", "operator": "greaterOrEqual", "value": 1000 },
    { "field": "signupDate", "operator": "withinLastDays", "value": 90 }
  ],
  "filterCombineMode": "AND",
  "sortBy": ["-revenue"],
  "distinctBy": ["email"],
  "limit": 500,
  "outputDatasetName": "clean-us-leads",
  "exportFormats": ["csv", "xlsx"]
}
```

### Output

Every kept, transformed row is pushed to the run's dataset:

```json
{
  "name": "jane doe",
  "email": "jane@example.com",
  "country": "US",
  "revenue": 12500,
  "signupDate": "2026-03-14",
  "domain": "example.com"
}
```

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel directly from the Apify Console, or request a ready-made CSV/Excel file via `exportFormats`. With **Also append to a named dataset** set, the same rows are appended to a dataset of that name in your account (created on the first run), so a scheduled pipeline accumulates into one place. A run summary (rows in, kept, excluded, duplicates removed, and any field a transform step couldn't honestly apply) is saved to the key-value store as `FILTER_TRANSFORM_SUMMARY`.

### Webhook destination

Set **Webhook URL** in the input and the kept, transformed rows (plus download links and a summary) are POSTed there as JSON the instant the run finishes, no need to poll the dataset or remember to check back. Works with a Zapier/Make/n8n catch-hook, your own API endpoint, or a Slack incoming webhook, so this Actor can be the last step in someone else's pipeline instead of a tool they have to run manually. A failed or unreachable webhook never breaks the run, it's reported as a warning in the output and costs nothing. Charged only on a confirmed delivery (see Pricing).

### Pricing

Pay-per-event, no subscription:

- **$0.002** per row kept (a row that passed your filter, survived sort/dedupe/limit, and was written out)
- **$0.01** per file export (CSV or Excel)
- **$0.02** per confirmed webhook delivery (only when the endpoint responds 2xx; a failed delivery costs nothing)
- A small per-GB run-start fee (the platform default)

A typical cleanup of a few thousand scraped rows down to the few hundred you actually wanted costs under a dollar. Rows that get filtered out, deduped or cut by the limit are never charged, and neither is downloading a file or appending to a named dataset.

### Works with the rest of the Nero Labs dataset toolkit

- [Dataset Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter): dedupe (exact, normalized or fuzzy), flatten nested JSON, clean emails, phones and URLs, then export CSV or Excel.
- **Dataset Filter & Transform** (this one): keep the rows you want and reshape the fields (dates, replace, split, hash, 26 ops), sort, dedupe, limit.
- [Dataset Join & Merge](https://apify.com/nerolabs/dataset-join-merge): VLOOKUP-style joins and unions across two datasets, files or Google Sheets on a key field.
- [Dataset Aggregate, Group By & Pivot](https://apify.com/nerolabs/dataset-aggregate-pivot): counts, sums, averages and pivot tables per group.
- [Dataset Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector): what was added, removed or changed since last time.
- [Dataset AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich): add LLM-generated columns (classify, extract, summarise) to every row, no API key needed.
- [Dataset Charts & Report](https://apify.com/nerolabs/dataset-charts-report): chart images (PNG, SVG) and a PDF or HTML report from any data.
- [Dataset to Postgres, Supabase & MySQL](https://apify.com/nerolabs/dataset-to-database): write the rows straight into a database table, creating it if needed.
- [Dataset to REST API](https://apify.com/nerolabs/dataset-to-rest-api): send every row to any API as its own request, with templating and auth presets.
- [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner): chain several of these together in one run, each step fed the previous step's dataset.

A common pipeline: a scraper, then Cleaner, then Filter & Transform, then Join to enrich from a sheet, then Aggregate for the weekly summary, with Diff watching what changed and Charts & Report turning the numbers into the Monday PDF. Pipeline Runner runs that whole chain in one call.

### Tips

- Filters run **after** transforms, so you can compute a field and then filter on it in the same run (see the default example: `cast` a price string to a number, then filter on the numeric value).
- Use `keep` as a last transform step to guarantee a clean, fixed column set for your CSV/Excel export, regardless of what extra fields the source data carries.
- `hash` over the fields that identify a record gives you a stable `rowId` you can dedupe or join on later, even when the source has no id.
- `maxItems` caps how many input rows are loaded, useful as a cost guard on very large inputs before you're sure the filter is right; `limit` caps the output.

### FAQ

**Does this work on any dataset or file?** Yes. This Actor processes only the data you already have (your own dataset, a file you link to, or pasted JSON). It doesn't scrape anything, so there's no target-site data-terms question to worry about.

**My Google Sheet link gives an HTTP 401 or 403.** The sheet isn't public. Set sharing to "Anyone with the link can view", or use File > Share > Publish to the web and paste that CSV link.

**What happens if a filter, sort or distinct field doesn't exist in my data?** You'll get a clear warning telling you no row had that field, so a typo doesn't just silently exclude everything with no explanation.

**What happens if `compute`, `cast`, `round` or `dateFormat` can't parse a value?** The field is set to `null` and counted in the run summary, never guessed.

**Can I run this on a schedule after another Actor?** Yes, that's the intended pattern: schedule your scraper, then schedule this Actor pointed at the same dataset (or chain them via an integration), and set a named output dataset so every run lands in one place.

If this saved you a spreadsheet filter-and-formula pass or a one-off cleaning script, a review on this page helps a small tool get found. Found a bug or want a feature? Open an issue on the **Issues** tab, replies come from a real person, usually within hours.

# Actor input Schema

## `datasetId` (type: `string`):

Pick an existing Apify dataset (for example the output of any scraper run). Use this OR 'File URL' OR 'Data (inline)' below. Declaring it this way is what lets this Actor run with limited permissions: it may read the dataset you point at, and nothing else on your account.

## `fileUrl` (type: `string`):

Instead of a dataset, download the rows from a public link: a CSV or TSV file, an Excel .xlsx file (first sheet, header row), a JSON array or JSON Lines file, or a Google Sheet (paste the normal sheet link, sharing set to 'Anyone with the link can view'). The format is detected automatically. Up to 100 MB per run. The download is never charged; only rows you keep are.

## `fileFormat` (type: `string`):

Only needed if automatic detection gets the file URL's format wrong.

## `data` (type: `array`):

A JSON array of records to filter and transform, for ad-hoc data instead of a dataset ID or file URL.

## `transforms` (type: `array`):

A list of steps applied to every row, top to bottom, before any filtering happens. Each step writes a top-level field. Dotted paths like 'address.city' can be read by copy, addField placeholders, compute, default, coalesce, replace, split, joinArray, substring, round, dateFormat, dateDiff, extractDomain, mapValues, hash, parseJson and length; rename, drop, keep, cast, trim, uppercase, lowercase and regexExtract only work on top-level fields, so copy a nested value to a top-level field first. Steps (26): rename {from,to}; drop {fields}; keep {fields} (drop everything else); copy {from,to}; setField {field,value}; default {field,value} (fill blanks); addField {field,template} ({{a}} {{b.c}} placeholders); compute {field,expression,round} (arithmetic + - \* / over numeric fields); cast {field,to: number|string|boolean}; trim / uppercase / lowercase {field}; replace {field,search,replacement,regex,into}; regexExtract {field,pattern,group,into}; split {field,separator,index,into}; joinArray {field,separator}; substring {field,start,length,into}; round {field,decimals}; coalesce {fields,into} (first non-blank); dateFormat {field,format: iso|date|unix|unixMs|'DD/MM/YYYY',into}; dateDiff {from,to (a field or 'now'),unit: days|hours|minutes|seconds,into}; extractDomain {field,into} (from an email or URL); mapValues {field,map,default,into} (look up values in a {from: to} table); hash {fields,algorithm,into} (a stable id for dedupe); parseJson {field}; length {field,into}.

## `filters` (type: `array`):

Rows are kept only if they pass these conditions (combined per 'Combine filters with' below). Each item: {"field": "revenue", "operator": "greaterOrEqual", "value": 1000}. Operators: equals, notEquals, contains, notContains, startsWith, endsWith, greaterThan, lessThan, greaterOrEqual, lessOrEqual, between (value = \[min, max]), isEmpty, isNotEmpty, isTrue, isFalse, matchesRegex (value = pattern), in, notIn (value = array), arrayContains, lengthGreaterThan, lengthLessThan, dateAfter, dateBefore (value = a date, 'today' or 'now'), withinLastDays, olderThanDays (value = number of days). Leave empty to keep every row (transform-only mode). 'caseSensitive' can be set per-condition to override the global default below; for matchesRegex only a per-condition 'caseSensitive': true makes it case-sensitive. Dotted paths like 'address.city' work in 'field'.

## `filterCombineMode` (type: `string`):

AND: a row must pass every condition. OR: a row passes if it matches any one condition.

## `caseSensitiveFilters` (type: `boolean`):

Off (default) treats 'US', 'us' and ' US ' as the same value for equals, notEquals, contains, notContains, startsWith, endsWith, in, notIn and arrayContains, which is what scraped or hand-entered data usually needs. Turn on to match letter case exactly. This is not byte-for-byte: surrounding spaces are still trimmed and repeated spaces collapsed, and equals, notEquals, in, notIn and arrayContains still compare two values that both read as numbers as numbers ('1,200' equals 1200 while 'Lenient numbers' is on). matchesRegex ignores this switch and stays case-insensitive unless that condition sets 'caseSensitive': true. Override per-condition with a 'caseSensitive' key on that condition.

## `lenientNumbers` (type: `boolean`):

Read numbers stored as text, like '$1,234.50', '49 USD' or '(300)', as numbers for numeric filters, sorting, 'cast to number', 'round' and 'compute'. Scraped prices almost always need this. Turn off to only accept real numbers and plain numeric strings.

## `sortBy` (type: `array`):

Order the kept rows. Each entry is {"field": "revenue", "direction": "desc"} or the shorthand "-revenue" (a leading minus means descending). Several entries sort by the first, then the next. Numbers sort numerically (with lenient parsing), text sorts case-insensitively, blanks always go last.

## `distinctBy` (type: `array`):

Field names. Only the first kept row for each distinct combination of these fields is written out; later duplicates are dropped (matched case-insensitively, trimmed, with '$1,200' and 1200 treated as equal). Combine with 'Sort by' to choose which duplicate survives, e.g. sort by date descending and dedupe by email to keep the newest row per email.

## `limit` (type: `integer`):

Stop after this many kept rows (after sorting and deduping). Useful for 'top 100 by price' style results and as a cost cap.

## `offset` (type: `integer`):

Skip this many kept rows before writing (after sorting and deduping), for paging through a large result.

## `outputDatasetName` (type: `string`):

Optional. A name (3 to 63 letters, digits or hyphens, e.g. 'clean-leads'). The kept rows are appended to a dataset with this name in your account, created on the first run, so a scheduled pipeline accumulates into one place instead of one dataset per run. Not charged again.

## `exportFormats` (type: `array`):

Also save the kept, transformed rows as a real downloadable file in the run's key-value store. CSV opens anywhere; XLSX opens in Excel and Google Sheets with a bold, frozen header row.

## `maxItems` (type: `integer`):

Stop loading after this many rows from the dataset or file (a cost guard for large inputs). There is a hard safety ceiling of 200,000 rows per run regardless.

## `webhookUrl` (type: `string`):

Optional. If set, the kept and transformed rows (plus download links and a summary) are POSTed here as JSON the moment this run finishes, so you don't have to come back and check the dataset yourself. Works with a Zapier/Make/n8n catch-hook, your own API endpoint, or a Slack incoming webhook. Only charged when the endpoint actually confirms receipt (HTTP 2xx); a failed delivery is reported as a warning in the run's output and costs nothing.

## Actor input object example

```json
{
  "fileFormat": "auto",
  "data": [
    {
      "name": "  jane doe ",
      "email": "JANE@Example.com",
      "country": "US",
      "revenue": "$12,500.00",
      "active": "yes",
      "signupDate": "2026-03-14"
    },
    {
      "name": "Bob Smith",
      "email": "bob@example.com",
      "country": "GB",
      "revenue": 400,
      "active": "no",
      "signupDate": "2026-05-02"
    },
    {
      "name": "Ana Silva",
      "email": "ana@example.com",
      "country": "BR",
      "revenue": "2,300",
      "active": "yes",
      "signupDate": "2026-06-19"
    },
    {
      "name": "  Chen Wei",
      "email": "chen@example.com",
      "country": "US",
      "revenue": "n/a",
      "active": "yes",
      "signupDate": "2026-07-01"
    },
    {
      "name": "Sam Lee",
      "email": "sam@example.com",
      "country": "US",
      "revenue": 9800,
      "active": "yes",
      "signupDate": "2026-08-05"
    }
  ],
  "transforms": [
    {
      "op": "trim",
      "field": "name"
    },
    {
      "op": "lowercase",
      "field": "email"
    },
    {
      "op": "cast",
      "field": "revenue",
      "to": "number"
    },
    {
      "op": "addField",
      "field": "summary",
      "template": "{{name}} ({{country}}) - {{email}}"
    }
  ],
  "filters": [
    {
      "field": "country",
      "operator": "equals",
      "value": "US"
    },
    {
      "field": "active",
      "operator": "equals",
      "value": "yes"
    },
    {
      "field": "revenue",
      "operator": "greaterOrEqual",
      "value": 1000
    }
  ],
  "filterCombineMode": "AND",
  "caseSensitiveFilters": false,
  "lenientNumbers": true,
  "exportFormats": [
    "csv",
    "xlsx"
  ]
}
```

# Actor output Schema

## `filteredRows` (type: `string`):

Every row that passed the filter, after the transform steps were applied.

## `csvFile` (type: `string`):

A ready-to-open CSV file of the kept, transformed rows, if requested.

## `xlsxFile` (type: `string`):

A ready-to-open Excel (.xlsx) file of the kept, transformed rows, if requested.

## `runSummary` (type: `string`):

Row counts in, kept and excluded, any transform steps that couldn't be applied to some rows, and warnings from this run.

# 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 = {
    "data": [
        {
            "name": "  jane doe ",
            "email": "JANE@Example.com",
            "country": "US",
            "revenue": "$12,500.00",
            "active": "yes",
            "signupDate": "2026-03-14"
        },
        {
            "name": "Bob Smith",
            "email": "bob@example.com",
            "country": "GB",
            "revenue": 400,
            "active": "no",
            "signupDate": "2026-05-02"
        },
        {
            "name": "Ana Silva",
            "email": "ana@example.com",
            "country": "BR",
            "revenue": "2,300",
            "active": "yes",
            "signupDate": "2026-06-19"
        },
        {
            "name": "  Chen Wei",
            "email": "chen@example.com",
            "country": "US",
            "revenue": "n/a",
            "active": "yes",
            "signupDate": "2026-07-01"
        },
        {
            "name": "Sam Lee",
            "email": "sam@example.com",
            "country": "US",
            "revenue": 9800,
            "active": "yes",
            "signupDate": "2026-08-05"
        }
    ],
    "transforms": [
        {
            "op": "trim",
            "field": "name"
        },
        {
            "op": "lowercase",
            "field": "email"
        },
        {
            "op": "cast",
            "field": "revenue",
            "to": "number"
        },
        {
            "op": "addField",
            "field": "summary",
            "template": "{{name}} ({{country}}) - {{email}}"
        }
    ],
    "filters": [
        {
            "field": "country",
            "operator": "equals",
            "value": "US"
        },
        {
            "field": "active",
            "operator": "equals",
            "value": "yes"
        },
        {
            "field": "revenue",
            "operator": "greaterOrEqual",
            "value": 1000
        }
    ],
    "exportFormats": [
        "csv",
        "xlsx"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/dataset-filter-transform").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 = {
    "data": [
        {
            "name": "  jane doe ",
            "email": "JANE@Example.com",
            "country": "US",
            "revenue": "$12,500.00",
            "active": "yes",
            "signupDate": "2026-03-14",
        },
        {
            "name": "Bob Smith",
            "email": "bob@example.com",
            "country": "GB",
            "revenue": 400,
            "active": "no",
            "signupDate": "2026-05-02",
        },
        {
            "name": "Ana Silva",
            "email": "ana@example.com",
            "country": "BR",
            "revenue": "2,300",
            "active": "yes",
            "signupDate": "2026-06-19",
        },
        {
            "name": "  Chen Wei",
            "email": "chen@example.com",
            "country": "US",
            "revenue": "n/a",
            "active": "yes",
            "signupDate": "2026-07-01",
        },
        {
            "name": "Sam Lee",
            "email": "sam@example.com",
            "country": "US",
            "revenue": 9800,
            "active": "yes",
            "signupDate": "2026-08-05",
        },
    ],
    "transforms": [
        {
            "op": "trim",
            "field": "name",
        },
        {
            "op": "lowercase",
            "field": "email",
        },
        {
            "op": "cast",
            "field": "revenue",
            "to": "number",
        },
        {
            "op": "addField",
            "field": "summary",
            "template": "{{name}} ({{country}}) - {{email}}",
        },
    ],
    "filters": [
        {
            "field": "country",
            "operator": "equals",
            "value": "US",
        },
        {
            "field": "active",
            "operator": "equals",
            "value": "yes",
        },
        {
            "field": "revenue",
            "operator": "greaterOrEqual",
            "value": 1000,
        },
    ],
    "exportFormats": [
        "csv",
        "xlsx",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/dataset-filter-transform").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 '{
  "data": [
    {
      "name": "  jane doe ",
      "email": "JANE@Example.com",
      "country": "US",
      "revenue": "$12,500.00",
      "active": "yes",
      "signupDate": "2026-03-14"
    },
    {
      "name": "Bob Smith",
      "email": "bob@example.com",
      "country": "GB",
      "revenue": 400,
      "active": "no",
      "signupDate": "2026-05-02"
    },
    {
      "name": "Ana Silva",
      "email": "ana@example.com",
      "country": "BR",
      "revenue": "2,300",
      "active": "yes",
      "signupDate": "2026-06-19"
    },
    {
      "name": "  Chen Wei",
      "email": "chen@example.com",
      "country": "US",
      "revenue": "n/a",
      "active": "yes",
      "signupDate": "2026-07-01"
    },
    {
      "name": "Sam Lee",
      "email": "sam@example.com",
      "country": "US",
      "revenue": 9800,
      "active": "yes",
      "signupDate": "2026-08-05"
    }
  ],
  "transforms": [
    {
      "op": "trim",
      "field": "name"
    },
    {
      "op": "lowercase",
      "field": "email"
    },
    {
      "op": "cast",
      "field": "revenue",
      "to": "number"
    },
    {
      "op": "addField",
      "field": "summary",
      "template": "{{name}} ({{country}}) - {{email}}"
    }
  ],
  "filters": [
    {
      "field": "country",
      "operator": "equals",
      "value": "US"
    },
    {
      "field": "active",
      "operator": "equals",
      "value": "yes"
    },
    {
      "field": "revenue",
      "operator": "greaterOrEqual",
      "value": 1000
    }
  ],
  "exportFormats": [
    "csv",
    "xlsx"
  ]
}' |
apify call nerolabs/dataset-filter-transform --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nerolabs/dataset-filter-transform"
        }
    }
}
```

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/4hGfRCihgwdC4fEI2/builds/rXrlN2QcMnBGSdaxd/openapi.json
