Ad copy is no longer given the wrong language. enrichment.language was
guessing on copy too short to guess from, and guessing wrong. Real examples from
production runs: "That first workout in your new gym fit" came back as German,
"Your go to gym set just got a summer upgrade" as Portuguese, "Fresh gear for
your next move starts here" as Danish, and "Get ready for election day" as
French. All four are English.
Two changes fix it. Handles, hashtags, links and emoji are stripped before
detection, since they carry no language signal but were padding short captions
past the length floor. And the floor is now counted in actual letters, raised
from 25 characters to 50 letters.
Galician was also removed from the candidate languages. It was beating Spanish
by 0.005 on plainly Spanish copy, and Meta ad targeting offers Spanish, not
Galician.
Measured against 16 hand-labelled ads, the old behaviour scored 8 correct and 8
wrong. The new one scores 8 correct and 0 wrong, declining to answer on the
8 it cannot call. Across 502 real ad copy blocks the two never disagree on a
language - the only change is that short or decorative copy now returns no
language instead of a wrong one, and coverage moves from 495 blocks to 460.
If your pipeline filters on enrichment.language, it will never see a
different language than before - only fewer rows carrying one. The
enrichment.version field moves from 1 to 2 so the two behaviours can be
told apart.
One summary row per advertiser. Switch on "Add one summary row per advertiser" and each advertiser in your results gets a single row covering all their ads at once: ad_count, active_ad_count, longest_running_days, first_seen, last_seen, distinct_landing_domains, distinct_copy_count, platforms, formats, and page and Instagram follower counts. None of this exists in an individual ad row - it can only be known once every ad has been collected.
The Instagram handle, follower count and verification status come from a company lookup the run already performed to resolve your brand name to a page ID. That lookup was already paid for and its result was previously discarded.
One summary row per landing page. Switch on "Add one summary row per landing page" and each distinct destination URL gets a row showing used_by_ad_count - how many separate ads point at it - along with the advertisers using it, its tracking parameters split into utm_source, utm_medium, utm_campaign, utm_content and utm_term, and where it resolves to.
Duplicate URLs are grouped. A carousel whose eight cards all share one link counts as one destination, not eight. These rows are not charged: they are the data your ad rows already contain, grouped by destination.
The destination an ad link actually resolves to. landing.final_url and landing.final_domain, plus landing.is_redirect when the two differ, so you can see past a tracker or shortener to the real page. Present only on ads where Meta publishes it.
More of what the source already sends. These arrive in the same response the run already pays for, and were previously dropped because they are empty on most ads:
creative.extra_texts and creative.extra_links - secondary copy and links carried alongside the main creative
creative.extra_media_count
signals.state_media_label - state-controlled media designation
signals.ec_certificates - EU e-commerce trader certificates
signals.report_count, signals.country_iso_code, signals.byline, signals.disclaimer_label
enrichment.scraped_at - when the row was collected
Separate creative, landing page and advertiser tables. Switch on "Also write separate creative, landing page and advertiser tables" and the run writes three extra tables next to your main results: meta-ads-creatives with one row per creative card, meta-ads-landing-pages with one row per distinct destination, and meta-ads-advertisers with one row per advertiser.
The creatives table is why this exists: a carousel with eight cards becomes eight rows there, while your main results keep one row per ad. Every row carries ad_archive_id so it joins back.
Not charged, and your main results are unchanged - the tables are written alongside, never instead. They are kept on your account until you delete them, under Storage > Datasets, so a scheduled run leaves a set behind each time it runs.
Two new dataset views, Advertiser profiles and Landing pages, so the summary rows are readable in the Console and exportable as their own CSV.
No input field was renamed, removed, retyped or made required, and no default changed. Both new options default to off, so a saved Task, schedule or API call that predates them produces exactly the rows it produced before.
Verified against 30 real ads: no enrichment field was removed and no existing value changed. An end-to-end test asserts that switching the extra tables on returns the same ads, in the same number, to the main dataset.
The Store page now shows an output preview. The Output tab previously rendered blank because no output schema was wired up. It now shows a real sample of what a run returns, so you can see the shape of the data before spending anything.
Internal API bookkeeping no longer reaches the run log. When an upstream request failed, the error log echoed the whole response body, including fields that describe this Actor's own API account rather than your run. Only the fields that explain the failure are logged now.
Paste a Meta Ad Library URL straight into the input. Copy a URL out of the Ad Library in your browser and drop it into searchTerms. The Actor reads the search terms, country, status and ad type out of the URL instead of treating the whole URL as a keyword to search for. A single-ad URL is recognised and looked up directly.
Launch recency flags. signals.launched_last_7_days and signals.launched_last_30_days, alongside the existing signals.days_since_launch, so new creative can be filtered without date arithmetic.
creative.copy_fingerprint. A stable hash of the normalised ad copy. The same creative running under different ad IDs, or across different advertisers, produces the same fingerprint - which is how you find a competitor running someone else's ad.
A size estimate before the run starts. A run combining several search terms, countries and languages multiplies out faster than it looks. The log now reports how many results the combination could reach before any of them are collected.
Contact details are no longer returned. Phone numbers, email addresses and postal addresses that occasionally appear in upstream page metadata are stripped from output rows.
Upstream API envelope fields are no longer written to the dataset. Each output row carried success, credits_charged and credits_remaining from the upstream response. These describe the API call rather than the ad, and they are now removed before the row is written.
If your pipeline reads any of those three field names, they will be absent from rows produced on or after 2026-09-03. No other field was renamed, retyped or removed.
Creative analysis (optional). A new "Add creative analysis fields" option adds columns built from data already in the response, with no extra API calls and about 1ms of work per ad:
creative.variants[] splits Dynamic Creative ads into their individual alternatives, each with its own copy, headline, button, destination and media. Meta buries these inside snapshot.cards[] where they are easy to miss. Verified on a 1,032-ad sample: 302 Dynamic Creative ads contained 704 distinct sub-creatives.
creative.media[] normalises image and video URLs into one list, including SD, HD, preview and watermarked variants.
creative.collation_id and collation_count for grouping related ads.
creative.partner names the paid-partnership page on branded content ads.
landing.domain, landing.path and any query parameters from the destination URL.
signals.days_running, plus signals.long_running which flags ads live past a threshold you set (default 30 days). Advertisers do not keep paying for creative that is not working, so a long-running ad is usually a proven winner.
enrichment.language, detected from the ad copy, since Meta does not provide it.
Switch the dataset to the Creative analysis view to see these as sortable columns.
AI analysis (optional, bring your own key). Two features that read the ad copy with Claude, using an Anthropic API key you supply in the input. Usage is billed to your own Anthropic account.
- Persuasion angle tagging: each ad gets
llm.primary_angle, llm.angles and llm.tone, drawn from offer, social proof, authority, fear, curiosity, urgency, problem/solution, testimonial, comparison, aspirational and educational.
- Advertiser strategy summary: one extra row per advertiser covering positioning, apparent target audience and patterns in how they build and test creative. Only produced when at least 10 ads were collected for that advertiser.
Distinct ad copy is only sent to the model once. Advertisers reuse copy heavily across ad IDs, so on the verified sample this removed about 80% of the work: 40 ads contained only 8 distinct copy blocks, turning 52 requests into 9.
Run summary on the run page. The status message now reports results returned, duplicate rows suppressed, countries queried, and AI classifications versus reuses, without opening the log.
The same ad is no longer returned, or billed, more than once. An ad that runs in several countries matched every country query and was written to the dataset once per match. Selecting four countries could return up to four copies of one ad. Results are now deduplicated within a run, and the count of suppressed duplicates appears in the run summary. Measured on a real multi-country run, roughly 10% of rows were duplicates.
Results appear as they are found. Previously rows were buffered and written near the end of the run. They are now flushed after every page, so data is usable while the run is still going.
Failed AI analysis no longer costs anything. If a classification request fails, the ad is returned without tags rather than failing the row or the run, and it is billed as a basic result rather than an enriched one. Requests are retried with backoff before giving up.
A strategy report is no longer discarded when the result limit is full. The report was written through the same counter as ad rows, so a run whose limit exactly matched the ads collected produced a report, paid the model for it, and then silently dropped it. Reports now bypass the ad limit.
Clearer reasons when AI analysis does not run. Missing keys, keys that arrive still encrypted, and values that are not shaped like an Anthropic key are each reported distinctly, in the dataset as well as the log, along with what to change. Previously all three produced the same opaque authentication error.
Charging is now reported accurately. The run summary previously reported charges for events the platform had skipped. It now reports what was actually charged, lists anything that was not, and stands down entirely rather than charging on top of Apify's built-in dataset-item event.
Error rows are never billed. Rows describing no results, missing input, API timeouts or skipped AI analysis carry no charge.
The input form is simpler. Only the two fields that apply to every mode are visible; the rest are grouped into Filter results, Limits, Extra data and AI analysis. Sixteen mode-specific fields now state which modes they apply to, so an option that does nothing in your chosen mode is identifiable at a glance.
No input field was renamed, removed or retyped, and the required list is unchanged, so saved tasks, API calls and integrations are unaffected.
Every limit field now defaults to 0, meaning everything. maxItems, maxItemsPerTerm and maxPagesPerQuery each default to 0, and 0 means no limit rather than no results. Field titles and descriptions state the convention explicitly, so a limit you did not set cannot silently truncate a run.
Example tasks. Pre-configured tasks covering the common jobs - competitor keyword search, tracking one brand's live ads, video creatives with transcripts, page ID lookup, political ad spend, EU DSA data and single-ad lookup. Open one, press Start, get results without filling in a field.
Free Apify accounts receive up to 50 results per run. Paid accounts are unaffected and receive results normally. This limit was set by the Actor developer, not by Apify.
A run no longer fails outright when a lookup fails. Instead of exiting with an error, the Actor writes a row naming what went wrong and what to change. A partial result set is returned rather than nothing.
Results are written in batches. Fewer, larger dataset writes, which makes long runs noticeably faster.
Empty values are stripped from output rows. Nulls, empty strings and empty arrays and objects are removed, so rows carry only fields that actually have data. This makes CSV exports substantially narrower.
Terms of Service. Full terms published in the README and the Information tab.
First release. Four modes against the Meta Ad Library through a commercial API - keyword search, all ads from a brand or page, brand page ID lookup, and full detail for a single ad by ID or URL. No Facebook account, no cookies, no login and no proxy configuration.