Clean, filter and dedupe Apify Dataset rows, then run an AI prompt on each row with no API key of your own. $1 per 1,000 rows kept, plus $1 per 1,000 AI rows, plus model usage on your Apify account.
New llm transform: one prompt per row, with {{field}} placeholders, through Apify's own apify/openrouter. No API key of your own is needed; the model usage is billed to your Apify account at the model's rate (the FREE plan pays 10x). The README has three recipes: classify Google Maps leads as B2B or B2C, pull salary, remote and seniority out of job posts, and translate reviews.
to writes a text answer. outputFields writes 1-10 typed answers ("string", "number", "integer", "boolean" or a list of allowed answers); an answer that does not fit leaves the field null and sets llmError, never a guessed value.
Filters that read fields the model does not change run before the call, so rows they remove cost no model call. A filter on the answer, for example "keep only B2B", runs after it.
maxLlmRows (default 1,000 calls), maxLlmCostUsd (default $1 of reported model usage) and llmConcurrency (default 8) bound each run. Identical prompts share one call (llmCache).
onError keeps a row without an answer (default) or leaves it out. If the first 20 rows sent to the model all fail with the same code, such as an unknown model id, the run stops before more calls and says what to change.
New event ai-row, $0.001 per row written with a usable answer. It is announced now and starts after Apify's 14-day notice; until then AI rows cost only the usual read and kept prices. A run without an llm transform costs exactly what it did before.
Documentation
New title: "Dataset Cleaner & LLM Processor: Filter, Dedupe, AI per Row". The technical name dataset-filter-transform, every existing input and every existing output field stay the same.
2026-10-28: Nested fields and type warnings (0.1.12)
Features
A dotted name such as price.value, metadata.title or authorMeta.fans reads the value nested inside objects, in filters, dedupeBy, transforms and {{placeholders}}. Before, it was read as a top-level field name, so such a rule matched no row, removed no duplicate or left the value as it was, with only a warning. A top-level field with that exact name still wins, so data with flat dotted column names reads as before.
A transform changes a nested value where it is. The source Dataset is never changed.
keep keeps a nested value under its path: ["url", "metadata.title"] writes {"url": ..., "metadata": {"title": ...}}.
rename moves a nested value to the top-level name in to, so metadata.title can become title.
A filter on a field inside an object that rename moved reads the new path. One inside an object that drop or keep removed fails before a row is read.
A field that does not exist yet, and every to, is still a top-level name.
Fixes
When a filter compares text such as "5" or "true" with a field that holds numbers or booleans, or a number with a field that holds numbers written as text, RUN_SUMMARY.inputWarning says so and shows the value to write, and nextAction points at it. Before, the run kept no row and suggested removing the filter.
Documentation
The crawler recipe keeps the page title from metadata.title. New recipes filter product rows by price.value and TikTok rows by authorMeta.fans.
2026-10-27: Renamed fields, regex groups and JavaScript replacements (0.1.11)
Fixes
A filter or dedupeBy on a field's name before a rename no longer fails the run. When only renames moved that field, the filter reads its new name, and RUN_SUMMARY.inputNotes says so: after status is renamed to state, a filter on status reads state. A field that a drop or keep removed, or that another transform changed after the rename, still fails before a row is read, and the message names the field to use.
A regexReplace replacement written in JavaScript, such as "$1", "$<name>" or "$&", now inserts the group instead of writing those characters into the row. It is read only when the pattern has that group, and the rewrite is listed in inputNotes.
Improvements
regexExtract takes "group", a group number or name, to write that group instead of the first group that matched. A group that did not take part in the match writes null. A group the pattern does not have fails before a row is read.
2026-10-27: More rules written by agents run as meant (0.1.10)
Fixes
A few common ways of writing a rule no longer fail the run. Each one is read in the documented form and listed in RUN_SUMMARY.inputNotes. A form that could mean two things still fails, and the message says how to fix it.
in and notIn take one value, such as "open" or 5, as a one-item list. Text with a comma still fails, because "open,new" could be one value or two.
between takes {"from", "to"} and {"low", "high"} like {"min", "max"}, and also the two ends written as rule keys.
JavaScript regex "flags" next to a pattern: i, m and s work as in JavaScript. A regex filter takes g or leaves it out. regexReplace replaces every match, so it needs g. regexExtract reads the first match, so it refuses g.
empty and notEmpty ignore "value": true.
cast reads "type" as "to". rename reads "newName" as "to". default reads "default" as "value". regexReplace reads a text "value" as "replacement". template reads "value" as "template" and "to" as "field".
2026-10-14: Integration runs start, and skipped rows say why (0.1.8)
Fixes
A run started from a scraper's Integrations tab no longer fails to start. The integration adds a payload object to the input, which the input schema refused; it is now accepted and not read. If the integration input has no datasetId, the run fails and says to set "datasetId": "{{resource.defaultDatasetId}}", instead of cleaning sample rows saved in the Task.
RUN_SUMMARY.skipCodes says why rows were skipped: EMPTY_ROW when the transforms left no field, NON_OBJECT when a row is not a JSON object. A successful run names the emptied rows in its next step.
A filter or dedupeBy on a field an earlier rename, drop or keep removed is reported together with the other rule problems before any row is read, so one retry fixes them all. A rename chain names every step and the last name.
Every RUN_SUMMARY.inputErrors entry has the same keys: rule, op, reason, key, accepts and message.
Improvements
When a number or boolean filter reads a field that holds "12.50" or "true" as text, inputWarning names the cast to add before the filter.
A template placeholder that no row has is named in inputWarning, like any other missing field.
A bare date as the upper bound of lte or between, such as "2026-10-31", gets a note in inputNotes: it means midnight UTC, and the note shows how to include the whole day.
The status line no longer starts with "datasetId is set, so the inline data was ignored" on every Console run with a picked Dataset. RUN_SUMMARY.inputWarning still says it.
The README has a recipe for cleaning prices such as "$1,299.00" before a numeric filter, and lists the 100,000-character text limit.
2026-10-13: Empty keeps and filters after a rename (0.1.7)
Fixes
A keep that names fields the rows do not have no longer writes an empty object for each row or charges the kept-row price. Those rows are not written. When the first 50 or more rows all become empty, the run fails before any row is charged and names the fields.
A filter or dedupeBy that reads a field an earlier rename, drop or keep removed fails before any row is read. The message names the field to use, for example state after status was renamed to state.
Documentation
The input schema describes date comparisons on gt, gte, lt, lte and between, including 2026-10-01 and now-7d.
The crawler recipe keeps url and markdown. A title stored at metadata.title is not read: only top-level fields are.
To clean each scraper run, save a Task without datasetId. On the scraper, open Integrations, connect that Task, and pass datasetId from {{resource.defaultDatasetId}}. A schedule does not fill that placeholder.
The limits section no longer says this Actor cannot deduplicate.
2026-10-06: Remove duplicates and filter by date (0.1.6)
Features
dedupeBy removes duplicate rows: list the fields that identify a row, such as ["email"] or ["url"], and only the first row for each value is written. Duplicates are charged as read, not kept, and counted in rowsDuplicate.
gt, gte, lt, lte and between compare ISO 8601 dates, and take "now-7d", "now-24h" or "now-30m" so a scheduled Task keeps filtering a moving window. If the field holds no ISO date in the first rows, such as Oct 5, 2026, inputWarning says so instead of a silent empty result.
Improvements
The README has recipes for cleaning crawler output for an LLM, preparing leads for a CRM, keeping the last 7 days, and checking rules on 20 rows first.
2026-10-06: Large rows and short timeouts no longer fail the run (0.1.5)
Fixes
A source of large rows, such as crawler output with html or markdown, no longer runs out of memory. Pages are now sized from the rows already read; before this, 1,000 rows of about 150 KB stopped the run at the default 512 MB.
A run that is about to reach its timeout stops 30 seconds early with stoppedReason: "TIME_LIMIT" and a PARTIAL summary, so the rows it wrote and the summary are kept.
When every row of a large source fails the same rule, the message shows the first value it read, for example "$12.99", so you can see what to clean.
Improvements
When keep or drop comes first, the fields they remove are not downloaded at all.
2026-10-05: Case-insensitive filters and input errors fixed in one retry (0.1.4)
Improvements
Text filters take "caseSensitive": false to match any letter case: equals, notEquals, contains, notContains, startsWith, endsWith, in, notIn and regex. "ignoreCase": true is read the same way.
trim, lowercase and uppercase take to to write the result to a new field and keep the original.
More rules run as meant, with a note in inputNotes: plain text in pattern for a text comparison, a JSON list written as text for in, notIn and between, and to as the value of default. Descriptive keys such as description or id are ignored.
Fixes
A run that fails on its input lists every rule problem in RUN_SUMMARY.inputErrors, with the rule, the reason, the key and the keys that op accepts, so all of them can be fixed in one retry.
equals without value now suggests notEmpty or empty when the aim is to check whether a field is set.
2026-10-05: Rules written by agents and integrations run as meant (0.1.3)
Improvements
A rule written in a form with only one meaning now runs instead of failing, and RUN_SUMMARY.inputNotes shows how it was read: a number written as text for gt, gte, lt, lte and between, {"min", "max"} for between, values for in and notIn, value as the regex pattern, "value": null on empty, notEmpty, true and false, int, float, bool or text as a cast target, from for rename, field for keep, and a missing replacement, which removes the matches.
cast takes integer as a target. A value with a fraction, such as 12.5, is a failed cast for that row rather than a rounded number.
trim, lowercase and uppercase accept fields to change several fields with one rule.
regexReplace, regexExtract and the regex filter read a number or boolean as its written text, so a phone number stored as a number can be cleaned.
The input form now lists the keys each op takes, and a filter without field is rejected before the run starts, so it is not charged.
A pattern written as a JavaScript literal, such as /abc/i, and a template with {single} braces are flagged in inputNotes, because both are read as plain text.
Fixes
An input problem names the rule, the key and the fix, for example filters[0]: unknown key "caseSensitive"; equals takes "op", "field", "value", instead of Extra inputs are not permitted.
A source of fewer than 50 rows in which every row fails the same rule finishes as PARTIAL with the failure codes instead of failing the run. The status names the most common failure.
A blank string in a number or boolean cast becomes null instead of dropping the row.
2026-10-03: Filter and reshape a Dataset without code (0.1.2)
Features
Paste rows or pick an Apify Dataset, then apply declarative transforms and filters. The run writes the kept rows and a RUN_SUMMARY with counts, transform failures and the next step.
You pay $0.00002 for each input row that is read and $0.00098 for each row that is written. A run that keeps every row is $1 per 1,000 rows. Filtered rows are not charged the kept-row price. The run also has the $0.00005 start. Set maxTotalChargeUsd before an API or agent run.
The sample input keeps one open company, trims its name, lowercases the email and renames company to companyName. Use the same rules from a webhook when the next scraper finishes.
Fixes
A pattern with several groups, such as (a)|(b), keeps the group that matched instead of an empty first group.
A replacement the pattern cannot apply drops each row it touches with REGEX_FAILED, instead of failing the run.
A regular expression that takes longer than 50 milliseconds drops that row with REGEX_TIMEOUT. The run continues, and stops after 200 such rows.
Null and missing fields are left as they are. A cast or text change fails only when the value has the wrong type.
If you set datasetId and the sample rows are still filled in, the Dataset is used and the pasted rows are ignored.
If every row in the first batch fails the same rule, the run fails before you are charged. A rule field that none of those rows have is reported in inputWarning and the run continues.
A schedule that writes to the same outputDatasetName every time keeps working when that dataset already holds rows from earlier runs.
A run that the platform restarts in the middle of a batch checks what that batch already charged and wrote, so the same rows are not charged twice.
Casting an integer with more than 15 digits drops that row instead of writing a value other tools cannot read.
The Actor refuses to write into the Dataset it is reading.