- The whole "Maximum cost per run" you set is now spent on transcripts. A capped run used to hold back about a tenth of your cap as a cushion against platform usage — but your cap pays for the transcripts and nothing else, so that tenth was cap you had asked to spend and did not get: a cap worth ten transcripts delivered nine. A run now transcribes every ad its cap can pay for, and still ends the way it always has — on its own line naming your cap and the exact count of what was left, never on a run the platform cuts short. Long videos are unaffected: the extra minutes they bill have always been held against the live room at the moment they are charged, and still are. Nothing about the price, the charged events, the input form or any output column changed.
- An ad page that named its video on somebody else's server could get that video transcribed and charged to you. It cannot now. This actor reads an ad's video from the Ad Library page LinkedIn serves, and it used whatever address that page handed it. A video address you paste yourself, or one that arrives in a chained scraper run, has always been checked — only a
dms.licdn.com/playlist/vid/… link is accepted — but the address lifted off the ad page itself was not, so anything that page pointed at was downloaded, transcribed, delivered as the ad and charged. From this build that address has to be on LinkedIn's own media CDN, exactly as a pasted one does. An ad page whose video sits anywhere else comes back as a single uncharged row that says so and names where a media file on any other host can be transcribed instead — never as "this ad has no video", never as something to re-run, and never as a charge. An ad page that offers several qualities and puts one of them elsewhere still delivers in full: the cheapest of LinkedIn's own is the one used, even when the other one is cheaper still. Every real ad is unaffected, and no price, input field, output column or charged event changed.
- A run that named a watchlist could stop dead in the middle of a delivery, and a refused list update said nothing. A watchlist is a key-value store in your own Apify account, and this actor writes to it right after each ad is delivered and before that ad is charged. If the API token the run was started with could read that store but not write it — which is what a token restricted with "Restrict what Actors can access using the scope of this Actor" does without key-value store Write and Create — the refused write ended the run there and then, with ads already in your results and no explanation on the run page. The run now finishes normally: every ad is delivered and charged exactly once, and the refusal is reported the way a refused account-memory update already is — on the run page, in one uncharged note row carrying the whole explanation, and in the run log beside Apify's own message — so you know those ads are not on your list and would be charged again next run unless the token is given key-value store Write and Create permission (or Actor runs is set to Full access). Runs started from the console or with a full-access token are unchanged, as is every price, input and delivered column.
-
A token that can READ your account's memory but not WRITE it was quietly re-charging you for every ad on your next run — the run now says so, and says what to grant. The memory that stops you paying for the same ad twice is a key-value store in your own Apify account, and an API token limited with "Restrict what Actors can access using the scope of this Actor" can carry key-value store Read without Write (or Write without Create on a store your account does not have yet). Such a run opened the memory, handed back every ad you already had, correctly — and then saved nothing, so your NEXT run downloaded, transcribed and charged for the whole delivery all over again, with no sign of it anywhere. From this build the very first refused write is named on the run page, in one uncharged row pushed the moment it happens, and in the run log beside LinkedIn's and Apify's own messages, each saying to give the token key-value store Write (and Create) under Settings → API & Integrations or set Actor runs to Full access. A run whose memory saves normally is unchanged in every respect, a passing network hiccup on a write is still just a hiccup, and no price, input field, output column or charged event changed.
-
A run started with a scoped API token now says why it could not skip the ads you already have — and how to fix it — instead of quietly charging for them again. The memory that stops you paying twice is a key-value store in your own Apify account, and a token limited under "Restrict what Actors can access" cannot open it unless it carries key-value store Read, Write and Create permission (or has Actor runs set to Full access). Until now such a run said only that the repeat check was "unavailable", which named neither the cause nor the cure: the same token was used again and the same ads were fetched and charged a second time. The run page now names the permission to grant and where to grant it, the run's first row carries the same sentence uncharged as a memory_note so a script or an agent reading only rows sees it too, and the run log keeps Apify's own message and adds the fix after it. Runs started from the console, or with a full-access token, are untouched. "New ads only" without a watchlist, which compares against that same memory, now stops with the same explanation instead of asking you to re-run in a minute. No input field, output column, charged event or price changed, and the run still delivers and charges exactly as it did.
- The store card and this page no longer promise a call-to-action on every ad, because only image ads actually carry one. The card said "plus CTA and advertiser data per ad" and the page said every ad comes back with its CTA. That is true of an image ad — the ad's own creative is read and its headline, body and call-to-action come back in
imageText — and it was not true of an ad this actor finds itself in the Ad Library, where ctaText is empty because LinkedIn's own ad page does not publish the button label. The field is unchanged and still filled when a run is chained after a scraper that carries it; what changed is that the copy now promises what the run delivers. The image-ad line is unchanged and still says headline, body and CTA, because that is exactly what comes back. The skipped-format note now says those rows carry advertiser and format metadata, which is what they carry.
- The table of sibling actors now names the Google Ads listing by its current title. That listing is called Google Ad Copy Scraper — Ads Transparency Center, CTAs & OCR; it was renamed so its name says what it returns — the text of an ad rather than "ads" in general. The link here points at the same actor as before; only the words a reader sees changed.
- Nothing else moved: no input field, output column, charged event or price changed.
- Pressing Start with nothing named, or with only a setting or two named, now comes back with the number of transcripts the sample promises, whenever there are enough speech-bearing ads in the search to draw from. The sample looks at a couple of extra ads so a music-only ad does not cost you a result — that arrived in the last version, and the run was throwing the extra ads away again before it used them. It no longer does. The sample still stops the moment it has the results it set out to deliver, still charges only for results actually delivered, still returns an ad with no speech as exactly that and uncharged, and still looks at no more than two ads beyond its own count. A run where you name your own advertisers, keywords or ad links is untouched.
- Nothing else moved: no input field, output column, event or price changed.
- Pressing Start without naming an advertiser or keyword now comes back with transcripts, not with an empty result. With nothing named, the run shows you a small sample of recent US ads — and its "Max items" count was spent the moment an ad was attempted, not when it delivered. Plenty of LinkedIn video ads carry music and no speech, so when the ads it happened to pick were all of that kind, the sample came back with uncharged "no speech" rows and no transcript at all, while ads that do speak sat unasked in the very same search. The sample now looks at a couple of extra ads and stops as soon as it has the results it set out to deliver. It still delivers the same number of ads it always promised, it still charges only for results actually delivered, and an ad with no speech still comes back as exactly that, uncharged. The extra look is strictly bounded: at most two ads beyond the sample's own count. A run where you name your own advertisers, keywords or ad links is untouched — "Max items" still counts ads, exactly as before.
- Nothing else moved: no input field, output column, event or price changed.
- The form now tells an AI agent what a run costs before it calls this actor. Apify's MCP server hands an agent the input form and never this page, so the description at the top of the form was the one screen that had to carry the whole story and did not. It now opens with the call that works, names what one delivered creative costs on the Apify free plan and on a paid one with the arithmetic for a thousand creatives, says plainly that image ads are charged the same way and that most LinkedIn ads are image ads, lists what is never charged, names the run option that caps the bill, and says where an ordinary media file or a Facebook Ad Library link belongs instead. Company names now leads with the value shape and says in its second sentence that searching is never charged; the "leave it empty and press Start" instruction moved to the end.
- Nothing else moved: no input field, output column, event or price changed.
- Two ads running the same creative now come back as two rows, each with its own ad. When two different ads in one run share one video or image, this actor transcribes and charges for that creative once — that has not changed and is not changing. What was wrong is which ad you got told about: only the first ad shipped a row, so if you had asked about the second one, the row you were handed carried the first ad's ad ID and the first ad's advertiser. Every ad you asked about now has its own row with its own ad ID, advertiser and details; the ad that shares the creative is marked
duplicate_creative with a new duplicateOf column naming the ad whose row holds the transcript, hook and on-screen text, and it is not charged. One creative, one charge, still — you just no longer get one ad's row standing in for another's. Rows that are not duplicates carry duplicateOf: null.
- A run chained to a dataset that turned out to be empty now says so, instead of finishing with nothing in it. If you pointed Dataset ID at a run whose results were empty, this actor read the dataset, found no ads in it, and finished with an empty dataset and no sentence anywhere saying what had happened — the same ending a broken run gives. You now get one uncharged row saying the dataset was read and held no ads, and suggesting the two things that fix it: check that the run you chained finished with results, or paste the ad links directly. Nothing was charged for those runs before and nothing is charged now.
- An ad already on your watchlist is recognised when it comes back by a different route, so you are not charged for it twice. An ad can reach this actor two ways — as an Ad Library ad with its own id, or as the media address of its creative — and which one you have depends on how you supplied it. Your named watchlist only ever remembered the route the ad arrived by, so the same ad supplied the other way looked new to it, was fetched and transcribed again, and was charged again. It now remembers every route to an ad and recognises it by any of them, in whichever order they turn up, and the row still tells you the date you first got that ad rather than today's. Two different ads that happen to share one video are still two ads, exactly as before. Your list keeps working as it is — nothing to re-create — and no input field, output column, event or price changed.
- An aborted or migrated run now leaves its receipt from the first moment of the run, instead of only after start-up finishes. The record that says how a run ended was only put in place once the run had finished starting up — reading your input, opening its stores, working out what a previous attempt had already delivered and charged. A run stopped inside that window ended with nothing written about it at all, which mattered most on a re-started run, where the thing not written about was the earlier attempt's work. It is now in place from the run's first moment. Nothing else moved: no input field, output column, event or price changed, and a run that reaches its work behaves exactly as before.
- A maintenance detail: the private cost record this actor keeps for our own accounting — our vendor spend, never anything you typed and never anything you are billed for — now counts the deeper read that a text-dense ad triggers, which it had been recording as costing nothing. Nothing a run does, delivers or costs you is different: no input field, output column, row status, event or price changed.
- The links to our other scrapers on this page name them correctly again. Several of those actors were retitled on the store, and this page still used their old names. The links always pointed at the right actors; only the words were out of date.
- An ad with a lot of copy on it now delivers its text instead of coming back empty. A text-dense creative — a long offer, a report-style ad, a dense international ad — could carry more text than our reader returned in one read, and the ad then shipped as an uncharged row with no text at all, as though nothing could be read on it. Such an ad now gets a second, larger read asking only for the visible text, and it delivers. Where an ad carries more copy even than that, the text ships cut short at about 3,500 characters with
truncated: true on imageText / onScreenText and a note on the row saying so, rather than being dropped. truncated is a new column inside those two objects and is false on every ordinary ad. An ad whose creative is too big to send to the reader at all now says that plainly, and is never charged.
- A number bigger than a field allows no longer stops the run before it starts. "Max ads to process" refused anything above 10,000, and the two impressions bounds anything above a billion — the platform checked all three before the actor was even started, so one extra zero on a hand-typed number (or a round one guessed by an AI agent calling through MCP) came back as an error with no run, no rows and nothing to read. The limits are unchanged and still real; they are now applied by the actor. A bigger ask runs at the limit — and an impressions filter set too high is kept at the limit rather than quietly dropped, so you never get back ads below the floor you asked for. Either way the dataset carries one uncharged row saying what you asked for and what the run used.
- A search this run's own limits cut short no longer reads as LinkedIn's answer about your advertiser. One search walks at most 40 pages, and a run stops searching when its time limit is close. When either of those ended a search, the row you got back said "No video or image ads found for advertiser X (40 pages searched). Check the spelling as it appears in the Ad Library, or try a broader query" — our limit, written as a verdict about your advertiser, with advice that could not move it. From this build the row names the limit that actually fired and the setting that lifts it (narrow the search, or raise the run timeout), and says plainly that it is a limit on this run and not a verdict about the advertiser. A search LinkedIn genuinely ran out of ads for, or genuinely answered with nothing, is unchanged — those rows read exactly as before. And a search that had already collected ads when one of those limits ended it now gets its own uncharged
search_cut note saying how many it collected and why it stopped, where before it said nothing at all and looked like a search that had run to the end. All of these rows are uncharged, as they were.
- An ad whose creative was too big for our text reader now gets read anyway. With on-screen text switched on, a long or high-resolution ad could be bigger than the reader accepts in one request, and the row then came back uncharged with no text on it at all — and running it again gave the same answer, because nothing about it was temporary. Such an ad is now sent once more at a smaller size, which is enough for almost all of them, and delivers its text. Where even that is too big the row is unchanged: uncharged, with no text, saying so plainly. Nothing is charged twice and nothing about the reading itself changed.
- Nothing else moved: no input field, event or price changed.
- The actor has a new name on the store: LinkedIn Ads Transcript Scraper — LinkedIn Ad Library to Text. Same actor, same URL, same input, same output, same price — the title now says what you get out of it. Nothing you have saved, scheduled or wired into an integration needs changing.
- AI agents can now pin this actor from the top of the README — the pin link, the actor id, the one input field to start from and how to cap a run's bill are on the first screen of the page instead of far down it.
- A maintenance detail: the private run-report this actor writes for our own support — counts only, never anything you typed — now also records the per-event price your plan was billed at, and whether a run that stopped at your Max ads to process had actually filled it. Nothing a run does, delivers or costs is different.
- Nothing else moved: no price, event, input field or output column changed, and only delivered rows are charged.
- Paste a video file that is not LinkedIn's and you are now told which of our actors does read it. This actor reads LinkedIn Ad Library creatives off LinkedIn's own CDN, so a link to a media file you host somewhere else — your own storage, your own CDN, a file an agent already downloaded — comes back as an uncharged row. It always did. What it did not do was say where that file CAN be transcribed, so the answer was a dead end: correct, and useless. From this build that row names our media transcriber, which reads an audio or video file from any host, and the exact field to paste the link into. LinkedIn links are unchanged — a mangled dms.licdn.com link still gets the LinkedIn guidance it needs, an Ad Library detail page is still looked up, a search link is still run as a search, and a plain LinkedIn page still gets its own answer. Nothing was charged for these rows before and nothing is now.
- Nothing else moved: no price, event, input field or output column changed, and only delivered rows are charged.
- A made-up closing line after the end of the audio no longer hides a real transcript — and is never delivered. Speech recognition sometimes invents a sign-off, most often "Thank you.", and stamps it with a time that falls past the end of the ad. When that stamp landed far enough out, the whole transcript was judged unreliable and the ad came back marked as having no speech: nothing delivered and nothing charged, even when there were 25 seconds of clear speech on it. It was found on a real customer run of our Facebook ads transcriber — a 30-second property ad with 25 seconds of clear voiceover that came back with nothing at all — and the same speech step runs here. From this build the invented line is removed before anything else is decided, so the real speech is delivered and charged exactly as usual — and the invented line is taken out of the transcript, the timed cues and the opening-line field too, so you are never handed, or billed for, a sentence the audio does not contain. The same invention was also slipping through quietly at the end of transcripts that DID deliver; it is gone from those as well. The length we measure for billing is unchanged.
- An ad that came back empty because of that fault is tried again, instead of being handed back with the same empty answer. Once an ad has been delivered to your account we never charge you for it twice, and to do that we remember what every ad answered. The trouble was that the memory also remembered the EMPTY answer — so re-running the same input handed the same nothing straight back from your own record, without fetching the ad at all. It was found on a real customer run of our Facebook ads transcriber, five runs in a row against the same ad, and the same memory works here. From this build, an ad whose remembered answer was an empty one — no speech found, no audio track, no on-screen text found, no text on the image — is tried again the first time you run it on a newer build, and charged once if it then delivers something. It could never have been charged before, because an empty answer is never billed. An ad ALREADY DELIVERED to you is untouched: it still comes back uncharged and is never charged a second time. So is a non-video creative, whose metadata row is the whole answer and cannot change — its ad page is not walked again. Switching on the on-screen-text option now also re-tries the silent ads that option exists to answer.
- Nothing else moved: no price, event, input field or output column changed, and only delivered rows are charged.
- A run that hits a block now waits a length that fits your run, instead of skipping the wait entirely. When LinkedIn blocks every attempt, the run pauses and walks the whole ladder again on a fresh session. Until now that pause was one fixed 90–180 second block: if your run's time limit could not hold a whole one, the run took no pause at all and you got "re-run in a few minutes" with almost all of your time unspent. The pause is now sized to the time your run actually has left, and the first one is short — about 30 to 60 seconds — so a block that lifts quickly costs you far less waiting. A short run, a small
maxItems or the untouched Start form now all get a real second walk where they used to get none.
- The waiting is also sized to what is still worth recovering, so a run with only a couple of ads left to read no longer spends eight minutes waiting for them — while a run that has not delivered anything yet always gets one pause whatever its size, because the first thing you see should not be an empty result.
- The run page no longer runs out of room before it gets to your totals. Apify cuts a run's status line at 500 characters. On a run with an ad page LinkedIn would not serve, this actor's line said the same ads twice — once as an uncharged miss and again as an unreadable ad page — and the sentence ran long enough that the reconciliation ("125 ads queued: 3 processed, 122 left"), the cap that stopped it and the "open an issue" line were cut off the end. Those ads are now reported once, in one clause that still names the walls and still tells you to re-run, and the sample preamble is a few words shorter. Every count, cap and charge statement is unchanged.
- We can now see WHY a run came back short. Every uncharged row already told you what happened to that ad; our own run record folded them all into one number, so a silent ad, an ad LinkedIn has taken down, a verification wall, a refused search and our own text reader failing all looked identical to us — and a problem worth fixing could sit unnoticed. Each is recorded separately now. Your rows, their wording and what you are charged are exactly as before.
- Nothing else moved: no price, event, input field or output column changed, and only delivered rows are charged.
- Two runs of the same watchlist started at the same time no longer erase each other's sightings. Watchlists and the account memory are now merged on every write, so an item one run delivered stays remembered, and a later re-run hands it back instead of charging it again.
- A dead ad-video link now tells you how to get a live one. When the media host refuses a video URL outright, the row used to say only that the URL was invalid or the media removed, and to pass the URL through unchanged. It now also says that LinkedIn signs creative URLs for a few weeks and that an aged link is fixed by re-running your Ad Library scraper for fresh URLs — the same remedy the row already gave for expired image links. Nothing was charged for these rows before and nothing is now.
- Nothing else moved: no price, event, input field or output column changed, and only delivered results are charged.
- Your account now recognises an ad however its link is written. LinkedIn serves creative files under several address shapes, and this actor could read the ad's identity out of only two of them. A creative arriving under one of the older shapes was remembered under its full download address instead — an address LinkedIn mints fresh every time, so the ad was never recognised again and a later run fetched, read and charged for it a second time. Every shape LinkedIn serves is now read the same way, and when a link carries no readable identity at all it is remembered by the file's own address with the single-use signature removed, which is an address a later run meets again. Re-running the same advertisers now hands back more of what you already hold, and charges for less of it.
- Other transcript actors' field names now work here. If you point this actor at input written for one of our other ad-transcript actors —
urls, adLibraryUrls, reelUrls for ad links, or advertisers for advertiser names — those fields are read as the ones this actor declares, and one uncharged row tells you which field name to use in a saved task or an API call. Nothing is renamed: every field this actor already published works exactly as before. A name that could mean two different things here is still refused rather than guessed at, because a run that answers a different question and bills for it is worse than one that says it did not understand.
- The store page now says how often re-running is worth it, and what a repeat run costs you.
- Nothing else moved: no price, event, input field or output column changed, and only delivered results are charged.
- An ad you re-run keeps the answer it actually got. When an ad comes back to you from the account memory — one this account already asked about — the row used to say "Already delivered to your account", whatever the first run had found. On an ad that was never transcribed at all (no speech, no audio track, nothing readable on screen, no text on the image, or a creative that is not a video) that was simply wrong, and it wiped out the sentence explaining why. Those rows now keep their own reason word for word and add one honest line saying it is the same answer as before and nothing was charged again. An ad that really was transcribed, or whose image or on-screen text was read, still says it was already delivered. Re-running now shows you at least as much as running once.
- Nothing else moved: no price, event, input field or output column changed, and only delivered transcripts are charged.
- A music-only or silent ad is no longer charged as a transcript. When an ad carried no speech, the transcriber sometimes invented a short caption for it — "Outro Music", "Música", "The End" — and that invented caption was delivered and billed as if it were the ad's own words. Short ads were where it happened: below fifteen seconds the only thing standing between an invented caption and your bill was a list of exact phrases, and anything not on the list went straight through. An ad whose transcript is nothing but a sound or a card is now recognised as such at any length and ships as an uncharged no-speech row — no result fee, exactly as the store page says.
- A short ad that really does speak is still delivered. Speech is now weighed against the part of the ad the words actually cover rather than the whole of its length, so a six-second ad carrying a three-word line reads as speech and not as silence. Short spoken ads that used to come back as "no speech" are delivered.
- An ad in any language is delivered in every market. A Russian-, Ukrainian-, Chinese-, Japanese-, Korean- or Arabic-language ad running in the US, UK, Canada, Australia, Ireland or New Zealand used to be treated as unreliable, returned with no transcript and no result fee, and remembered that way for the next run. That rule is gone. An ad is transcribed and delivered in whatever language it speaks, wherever it runs.
- Nothing else moved: no price, event, input field or output column changed.
- A run that answered every ad without charging you no longer asks you to report a problem. When an advertiser's creatives are all formats this actor does not read — documents, carousels, articles, messages, text-only updates — each one comes back as an uncharged metadata row and nothing is charged. When you re-run ads your account already has, they are handed straight back, also uncharged. Both endings are the actor doing exactly what you asked, and both used to close with "Something off? Open an issue". They now read as what they are. A run that genuinely hit something — an unreadable ad page, a search LinkedIn would not serve, a failure, an input mistake, your max charge or the run timeout — still says so and still points you at the Issues tab.
- The summary's count and its verdict now come from the same place. The clause that reconciles "125 ads queued: 3 processed, 122 left" and the decision about whether the run went wrong used to be worked out separately, so one missing category could make both wrong at the same time. They now read one ledger, and ads the run cannot account for are what it asks about — never the fact that nothing was charged.
- Nothing else moved: no price, event or output column changed, and only delivered results are charged.
- Re-running the same ads no longer re-opens the pages that were already answered. A creative that is not a video — a document, carousel, article, message or text-only ad — comes back as an uncharged metadata row, and that answer never changes. Every repeat run still opened that ad's page on LinkedIn to be told the same thing. Your account's record of the ads it already has now keeps that answer too, so a repeat run hands the row straight back. Same rows, no result fee for them before or now, and one less request to LinkedIn per ad.
- That record now holds about twice as many ads before it starts forgetting the oldest. Each ad used to take two entries in it — one of them a single-use download address that can never turn up again — so the record filled at half the ads it should hold, and what it dropped to make room was the half that still mattered. Each ad now takes only the identities that last, which is what keeps you from paying for the same creative twice.
- A repeat run's summary now counts an ad it handed back as one it processed. Those ads were reported as "left", which read as though the run had stopped early. Nothing about the rows or the charges changed — the count now matches what you got.
- Nothing else moved: no price, event or output column changed, and only delivered results are charged.
- A media host we cannot reach directly is no longer reported as a host that does not exist. Some servers publish only one kind of internet address, and it is a kind our machines cannot dial straight out. The video and image download used to demand the other kind, get "no such address" back, and answer as though the creative were gone. It now prefers the address it can dial rather than demanding it, so LinkedIn's media host is reached exactly as before and a host that ever stops publishing that address comes back with what actually happened. Nothing was ever charged for either row.
- Nothing else moved: no price, event, output column or charge changed, and only delivered results are charged.
- A block of links pasted into one row is now read as the list you meant, whatever separates them. "Ad URLs, video URLs or search links" takes one entry per row. A whole block pasted into a single row was read as one very long address, so a list of fifty ads came back as a single row calling them all a LinkedIn page. A pasted block is now split back into its individual links whether they are separated by spaces, line breaks, tabs, commas, semicolons, pipes or nothing at all, so a column copied straight out of a spreadsheet works. Each one is then read, deduplicated and charged on its own, exactly as if you had pasted them one per row.
- A link that carries another link inside it is still one link. An address holding a second address in its query or its path is left whole, not broken in two, and a single link pasted on its own is never rewritten.
- Text pasted around a link no longer breaks it. A number, a bullet or a note sitting beside a link is ignored and the link itself is used. A row holding no link at all is still answered as one row, not one row per word.
- Nothing else moved: no price, event, output column or charge changed, and only delivered results are charged.
- Every ad the run timeout left behind now arrives as its own row. When a run reached its time limit before an ad's turn, that ad was simply absent: the run page said how many were left, but your dataset held nothing for them, so a list of ten ads that ran out of clock after three gave you three rows and no way to see which seven were missing. Each one now ships as an uncharged
skipped_deadline row carrying everything the search already knew about it — the ad id, its page link, the advertiser — and a reason saying the run reached its time limit before it was fetched.
- The row tells you how to get it: raise the run timeout (or split the batch) and re-run, or pass that row's
detailUrl back in videoUrls.
- Nothing was fetched and nothing is charged for these rows — the ad was never started, so there is no result fee and no extra request to LinkedIn.
- Ads beyond your own
maxItems cap are unchanged: that cut is yours and stays a named count on the run page, with no row.
- Nothing about pricing, events or output columns changed.
- An ad that was already transcribed or read when your cost cap was reached now reaches you. If a run hit its Maximum cost per run in the moment an ad's transcript — or an image ad's on-image text, or a silent ad's on-screen text — had just come back, that result was thrown away and you got an empty "cost cap reached" row in its place. You paid nothing for it, but the answer you had waited for was gone, and only a re-run at a higher cap could bring it back. That ad now arrives complete, with
charged: false and a row saying the cap was reached and nothing was charged for it.
- Your maximum cost is still never exceeded — the cap takes the charge, not the result.
- Ads the cap stopped before any work was done are unchanged: they still ship as uncharged
skipped_budget rows naming the cap, so you still get exactly one row per ad you asked for.
- Nothing else moved: no price, event or output column changed, and you are still charged only for ads that actually deliver.
- A blocked image download no longer ends that ad in about a second. Reading the text on an image ad starts by downloading its creative. When LinkedIn's media servers turned that download away, the run tried three times inside roughly a second and handed back an uncharged "please re-run" row — while it was still holding nearly all of the run time you gave it. It now waits a minute or two and fetches the creative again, for as long as your own run time allows and never into the time it needs to write your rows and finish honestly. Searches, ad pages and video downloads already worked this way; image ads did not — and image text is charged exactly like a transcript.
- Nothing that already has its answer waits. An expired creative link, a link that serves something other than an image, and every verdict from the text reader still come back immediately. Waiting buys time, never extra downloads: a blocked creative is fetched at most three times, the same as before, and the run can never spend more than its own patience allowance however many ads it meets.
- A row that asks you to re-run an image ad now carries the
retryable flag that says so. Those rows were uncharged before and still are.
- Nothing about pricing, input or output columns changed. Waiting is uncharged, a creative that never arrived is uncharged, and only ad text delivered into your dataset is charged.
- The run's own record now says how many ads it was asked for, and names every ad it did not deliver. A run that searched the Ad Library reported its ask as the max-items number — 1,000 by default — however few ads the search actually found, so a search that turned up six ads read as six of a thousand. The ask is now what the run really planned: the ads each search found, the ad links and dataset rows you sent, one for each search that came back with no ads and one for each search LinkedIn refused.
- A non-video ad now counts as answered. A carousel, document, text or image creative comes back as an uncharged metadata row — a real answer to what you asked — and those rows were counted nowhere. They now count as delivered, which is what they are, alongside ads an earlier server of the same run had already delivered.
- Every ad the run planned and did not deliver is counted under its own reason — skipped by your "new ads only" filter, or left behind when the platform moved the run to another server.
- The run also records what stopped it (your max-items cap, your max charge, the run timeout, a move to another server, a mis-priced rail, a resume it could not read, or nothing — the work list simply ran out). This was declared and never written, so every run's record read as if nothing had stopped it.
- A search stopped by a setting is recorded against that setting, so one date filter this actor cannot read reads as one strict setting rather than as unexplained missing ads. The run page's wording is unchanged: it still says one input error, however many searches that refusal stopped.
- Nothing about your results or your bill changed — no row, price, event, column or charge. Delivered transcripts are charged; metadata rows, misses and skips stay uncharged.
- A run that waits now always keeps room to finish and report. When LinkedIn walls every route to a page, the run can pause and climb the whole ladder again on a fresh session — but the check that decided whether a pause was affordable kept back only half a minute for that second climb, while a real one takes about a minute and a half. On a short run — the five-minute limit the daily health check uses among them — a shorter pause could be allowed and the climb after it then ran past the end of the run, so the honest uncharged row it was taken for never landed. The check now keeps back a whole climb, measured from the ladder's own rungs, timeouts and pacing, so a pause is only taken when the run can finish the climb the pause was for.
- A run you start with nothing set now gets seven and a half minutes instead of seven. The sample's own limit had been sized against the half-minute placeholder; it is now long enough to hold the first walled climb, one pause of up to three minutes and a real second climb, with time kept back to write your rows and the summary. A sample LinkedIn answers straight away still ends in about twenty seconds — the limit is a ceiling, never a wait.
- Charges are unchanged. Waiting is uncharged, a walled page is uncharged, and only delivered ads are charged.
- A block on LinkedIn's side no longer ends your run in ten seconds. When the Ad Library refused every connection this run could make, the run gave up almost at once and asked you to re-run in a few minutes — while the run itself was holding those minutes, unspent. It now waits a minute or two, starts again on a fresh connection, and searches the whole way through a second time, for as long as your own run time allows and never into the time it needs to report honestly. An ad that is gone, a search LinkedIn answered with no results, and a mistake in your input are still answered immediately, exactly as before. Nothing was charged for a blocked search before and nothing is now.
- A run started with nothing set now has the time to wait one out. Pressing Start on the untouched form reads a small sample inside its own short window — and that window was shorter than a single blocked walk through our connections, so on a blocked minute the waiting above could never have happened on the run most people see first. The window now holds one blocked walk, one wait, and room to deliver afterwards.
- The default sample reads fresh ads again. A run with nothing set (or with only a setting changed) no longer answers from the record of what your account already had — it reads current ads and charges for them like any run. Runs that name advertisers, keywords, ads or a chained dataset are unchanged: an ad your account already holds is still handed back from the run that delivered it, uncharged.
- A row that asks you to re-run now carries the
retryable flag that says so — a search LinkedIn blocked, and an ad page it answered with a verification screen. Both were uncharged before and still are.
- The default sample delivers again for accounts that have not run it before. Since 1.0.64 a video ad is only downloaded when the run has enough time left to download and transcribe it in the worst case. The bare-Start sample (nothing set, or only a setting changed) runs under a short three-minute window, and that window was shorter than that worst case — so on an account that did not already hold the sample's creatives, every video ad in it came back as an uncharged "the run reached its time limit" row within seconds. The worst-case check now reads the run's real time limit; the three-minute window still bounds how long the sample keeps starting new ads. Runs that name their own advertisers, keywords or ads were never affected, and nothing about pricing, input or output changed.
- An ad near the run's time limit now stops cleanly instead of being cut off mid-transcription. Fetching the ad page, and downloading and transcribing its video, take time; if the run does not have enough left, that work is not started — the ad comes back as an uncharged "please re-run" row (you can also raise the run timeout) rather than a run cut short. A run that reaches its limit ends with the transcripts it already has.
- The status line of a settings-only run no longer states a creative count your own cap can contradict. When you set "Max ads to process" below the sample's size, the run read that many — the opening sentence still said "up to 3". It now names the market and the window only; the uncharged note row and the rows themselves carry the size the run actually read. Nothing about pricing, input or output changed.
- Change only an option, name nothing, and the run now goes ahead. Turning image-text or on-screen-text reading on or off, setting a cap, "New ads only" or a watchlist name and pressing Start without naming an advertiser, keyword, market or ad used to turn the run away with a single uncharged guidance row — so the buyer who flipped one toggle got less than the buyer who touched nothing. Those settings are now applied to the default 3-creative sample and the run delivers, charged like any run. A cap smaller than the sample is honoured exactly; a bigger one never grows it.
- One uncharged
sample_note row says which settings were yours, what the sample supplied for the rest, and how to run your own search. It is never counted as a miss, and the run status line says the sample ran under your settings.
- Guidance is unchanged where it is the honest answer: a field name this actor does not recognise, or a list you sent empty (
accountOwners: [], keywords: [], countries: [], datasetId: "") still gets its uncharged row naming the fix — a mistake is never answered with sample creatives. A market filter on its own is unchanged too: it has always been a complete search of that market, never the sample.
- Pressing Start with nothing set at all is unchanged: the same 3-creative browse of the US market's last 30 days.
- A date window this actor cannot read no longer swallows your whole search list. "Date range" scopes every search a run makes, so an unreadable one stops the searching — that part is right, and searching a different period would answer a different question. But until now the run emptied the list: eight advertisers and keywords came back as ONE guidance row. Every search you asked for now gets its own uncharged row saying it was not run and which field stopped it, and the status line counts them apart from the input error itself.
- A country the Ad Library does not offer now says which of three things went wrong — a real country it simply does not carry as a market (so fixing the spelling will not help), a region-wide ask like "EU" on a field that filters one country at a time, or a value nothing could place. Ordinary spellings (
US, USA, United States, uk, GBR, Deutschland) still resolve and are never refused.
- Every input problem now names the field it was about in the run's own record — the countries, the date window, the impressions range, the ad links, the advertiser field, a watchlist name — instead of one undifferentiated "input error". The field NAME only; nothing you typed is stored.
- The run's record now reports what you ASKED for alongside what was delivered, so a run that turned everything away can be seen as one. It previously reported the queue AFTER already-delivered ads were dropped, which is 0 on exactly the runs worth looking at.
- The Changelog tab no longer describes any row as "free" — the word is "uncharged"; running any actor still uses platform time you pay for. No prices, counts or behaviour changed.
- A date window written the ordinary way now works in "Date range".
Last 30 days — the console's own name for the option — plus last 30 days, This month and Last year used to count as unrecognised values, and an unrecognised window refuses every advertiser and keyword search in the run: one guidance row, and nothing for anything you sent. They all land on the window they name now. A window the Ad Library does not offer is still refused rather than searched over some other period.
- A country written the ordinary way now works in "Countries".
UK, USA, United States, Germany and Brazil used to be dropped — and a dropped country meant the search still ran, with no country filter at all, over a market you never asked for. They resolve to the code the Ad Library lists now. A country the Ad Library does not offer is still reported and never searched around.
- README wording: every uncharged case on this page now says so plainly. Seven lines described a row, a verdict or a pass-through as costing nothing; each now says uncharged, the same word the rows and the run status already use. Nothing about pricing, events, input or output changed.
- An ad already delivered to your account is never charged a second time. Re-running the same Ad Library links, IDs, search or chained rows used to fetch and charge every ad again. The run now recognises an ad your account already has — under its Ad Library ID, its creative's asset ID or its media URL, however it arrived last time — and hands it back from the run that delivered it:
repeat: true, firstSeenAt, firstSeenRunId, charged: false, not fetched or transcribed again. The status line counts them and OUTPUT.repeats holds the number. Your account keeps this memory in the key-value store linkedin-ads-watch-account; delete it to forget everything.
- New ads only works without a watchlist name. It compares against your account's memory; a named watchlist still keeps a separate list per competitor set. Only when neither can be read does the run stop, uncharged, instead of charging you for ads you may already have.
- If the memory cannot be read, the run still runs. It delivers and charges as usual and says on the status line and the charged rows that the repeat check was unavailable.
- Every row now carries
isNew and firstSeenAt, not only rows of a watchlist run.
- A watchlist written before this build kept no copies of its rows: the ads on it are processed again once, not charged, and kept from then on.
- Rows pasted into the "Dataset ID" field are read as your rows. A paying buyer pasted a dataset's exported rows into that field; the run sent them to the platform as if they were an ID and stopped on the answer. The run now recognises pasted rows there, works them exactly like rows pasted into "Dataset items", and adds one uncharged note saying where they belong next time. Anything else that is not a dataset ID or dataset name — a link, a value with spaces, a very long value — is answered with one uncharged row naming what it looked like, before anything is sent anywhere. Nothing was charged for the runs that stopped, and nothing is charged for these notes.
- A Dataset ID the platform itself cannot answer for is settled in seconds, not minutes. When the platform's own dataset lookup fails on its side, the run used to wait through the platform client's long retry schedule — close to seven minutes on one lookup — and could then run out of its own time. The lookup now gets two quick tries plus one short pause, and the run moves on to its honest uncharged row. Nothing about what is charged changed.
- A guidance row is never dropped by the run's time limit. A row that only explains an input problem costs nothing and takes no time, so it now ships even when the run has reached its safety margin; only real work is skipped there.
- A chained Dataset ID the run cannot read now ends as one uncharged row, whatever the reason. Until now a refusal the run did not recognise — anything other than "no such dataset" or "no access" — stopped the run within seconds with nothing delivered. Every answer now becomes an honest
input_error row naming what the platform said, a momentary refusal gets one more try, a read that stops part-way keeps the rows already read and says where it stopped, and the run carries on with the rest of the input. A value that is not a dataset ID at all says so instead of reading as a temporary problem. Rows chained in an unexpected shape were already harmless and now have a test that keeps them so. Nothing was charged in any of these cases before, and nothing is now.
- A run that stops on a fault of ours now tells us what kind of fault it was, so it is looked at without anyone needing to share the run. Nothing about what is delivered or charged changed.
- A problem inside the speech-to-text service is no longer reported as a problem with the ad's audio. Every refusal from that service used to read the same way — a permanent, uncharged miss saying the creative held nothing we could transcribe — so "the service changed something on its side" and "this file has no usable audio in it" looked identical in your dataset. A fault on the service's side now ships as an uncharged row marked
retryable that asks for a re-run, and a file we genuinely cannot transcribe keeps the honest, uncharged miss it always had. Nothing about what is charged changed.
- A run that stops before it starts now reports its outcome too — a run refused for its memory setting, and a run stopped because a key on our side is missing, now reach us the same way every other run does: counts and reason codes only, never your input or your rows.
- Every run now reports its own outcome to us — counts and reason codes only, never your input or your rows — so a run that goes wrong reaches us even when nobody shares it.
- A chained Dataset ID is looked up read-only. A mistyped ID creates nothing and returns one uncharged input-error row that names the ID as the problem; a real dataset is read page by page with an honest notice past 10,000 rows.
- The watchlist clause of the status line no longer splits itself with a semicolon.
- Row notes state what happened and what was charged; the support ask lives on the run page.
- The console's input and output descriptions no longer call rows "free" — they are uncharged; the run still uses platform time.
- Rows that carry no result fee are now described as uncharged, not free — running any actor still uses platform time, so no row is ever free.
- The run summary again shows the full queued-versus-processed reconciliation, which the old longer wording pushed off the end.
- The end-of-run summary now fits on the run page. It was long enough to be cut off part-way through; the wording is tighter and every count, cap and charge statement survives.
- A run that ends with a problem now says where to reach us. A miss, an input error, an early stop or a failed run closes by pointing at the Issues tab and naming the reply time; a run that delivered everything, including one that filled the row cap you set, is left alone.
- One support promise across this page — issues are answered in a couple of hours, always within a day.
A run that resumes after a platform restart now recognises every row it already delivered, so nothing is delivered or charged twice.
Apify occasionally moves a running actor to another server. When that happens, the run re-reads its own dataset to remember what it already delivered. Until now it trusted the dataset's row count, which can lag for a moment after a restart; a lagging count could make the run start over and charge again for rows you already had, or stop reading before the end. The run now checks for real rows instead of trusting the count, and reads to the end whatever the count says. Rows, prices, charges and the status line on a normal run are exactly as before.
Internal bookkeeping only — nothing changes in your rows, prices, charges or status line.
The record this actor keeps of its own running costs now reports itself: if it cannot be saved, the run log says so plainly instead of staying quiet, and every run's OUTPUT record carries whether it was saved. Your dataset, your charges, your prices and the status line are exactly as before.
A date range the actor cannot read now stops the search instead of searching a different one.
A value the Date range dropdown does not offer — or a start/end date that cannot be read — used to be flagged and then ignored, so the run searched LinkedIn's default date window and charged you for ads from a period you never asked for. It now returns one uncharged row naming the value you sent and the options that work (last-30-days, current-month, current-year, last-year, custom-date-range), and no search runs. Ad links, ad IDs and a chained dataset in the same run are unaffected and still processed. Leaving the field empty is unchanged: no date filter, no row.
Searches and ad pages now survive a blocked connection.
When LinkedIn refuses the connection a run is using, the run switches to a stronger route instead of giving up after one retry — so searches and ad pages that came back "couldn't be reached" return their creatives again. Runs that still cannot reach the page are unchanged: one honest row saying so, and nothing charged.
Documentation only — nothing changes in your rows, prices, charges or status line.
The example screenshots on this page now load from Apify's own storage, and the references that used to point off Apify are plain text now. The free n8n workflow templates are unchanged and still listed on our profile website. Your dataset, your charges and the status line are unchanged.
Internal accounting fix — nothing changes in your rows, prices, charges or status line.
A small correction to how our own cost records count very short clips, and our cost records are now complete for every run. Your dataset, your charges and the status line are unchanged.
Internal cost accounting only — nothing changes in your rows, prices, charges or status line.
Runs started from our own account now record the processing they used, so our cost reports are measured instead of estimated. Nothing is added to your run or its storage, and your dataset, your charges and the status line are unchanged.
Start with the default form and get a real 3-creative sample instead of a placeholder row.
Clicking Start with nothing set used to return one uncharged "demo" row and no transcript. It now runs a small real sample — a browse of the United States' Ad Library, last 30 days, up to 3 creatives — read and charged like any run, so the first thing you see is the actual output. The status line says it was the sample and how to run your own search. If the Ad Library cannot be searched at that moment, you still get one uncharged row explaining it, never an empty result.
Text-heavy creatives are now read in full.
An image ad or a silent video ad carrying a lot of on-screen copy used to come back as an uncharged "service problem, please re-run" row, and a re-run gave the same answer. The text reader now gives such a creative a second, larger pass, so it delivers like any other. A creative that still cannot be read reliably ships as an uncharged row that says so, never as a charge.
Watchlists: re-run the same advertisers on a schedule and pay only for the ads that are new.
Until now every run was a one-off. Ask for the same advertiser tomorrow and you got — and paid for — the same ads again. Two new fields change that:
- Watchlist name (
watchlistId) — give the run a name such as crm-competitors and the actor remembers every ad it has answered under that name, in a key-value store in your own Apify account (linkedin-ads-watch-<name>, yours to inspect or clear). Every row now says whether the ad is new to that list (isNew) and when it was first seen (firstSeenAt).
- New ads only (
newAdsOnly) — with a watchlist set, ads already on the list are skipped before anything is fetched: not fetched, not read, not charged. The run's status line and its OUTPUT summary say how many were new, how many were skipped, and how many the list holds now.
Only an ad the actor actually answered goes on the list — a delivered transcript, image text or on-screen text, or a final uncharged verdict such as no speech or a text-free image. An ad it could not answer (a blocked page, an expired link, an ad your cost cap left out) is not remembered, so the next run tries it again. The list is written the moment a row lands in your dataset and before its charge posts, so an interrupted run can never forget an ad you paid for.
Two safety rules: New ads only without a watchlist name does not run at all (the actor cannot tell a new ad from one you already paid for), and a watchlist the run cannot open stops the run before any spend. Both ship as one uncharged, clearly-labelled row.
Without a watchlist name nothing changes: the two new columns read null, and every input mode, price and charging rule is exactly as before.
The README was refreshed at the same time: what you get and the price up front, a scheduling recipe for the watchlist, the one-click MCP link, and the suite links carry the actors' current names.
The step that reads text off a creative moves to our provider's current engine.
The engine behind image-ad text extraction — and behind on-screen text on silent video ads, if you have that turned on — is being retired by the provider this month, with runs quietly redirected to its replacement two weeks from now. This build makes that move explicitly instead: charged output should never ride a silent switch.
Both engines were run side by side on the same real LinkedIn creatives before this shipped, including the silent case-study and static-card ads this actor sees a lot of. The same text comes back, and the rule that matters most is unchanged: a creative with no readable ad copy — office b-roll, a bare logo plate — is still an uncharged, honest miss and is never charged. On a text-heavy image ad the new engine fills in the headline field a little more often. Nothing about what this actor charges changed.
Long video ads no longer come back as failed_processing.
This actor gave itself a fixed two minutes to pull the audio out of a creative, no matter how long that creative was. It was set once, for short ads, and never grew with the video — so a long-form creative was stopped partway and reported as a broken file. LinkedIn allows video ads up to thirty minutes, so this was live exposure here. A sibling actor on Facebook inventory measured it on a real run: every creative past roughly twelve minutes was lost, and nothing under five minutes ever was.
- The time budget is now earned by the video's own length, up to a generous ceiling, and it is also held inside what is left of your run.
- A silent creative is recognised before any extraction work starts — the uncharged
no_audio_stream row (and the on-screen-text result behind it, if you have that turned on) is reached from the file's own stream list rather than after a failed attempt.
- "Ran out of time" and "this file is broken" are now two different answers. A run that ran out of time ships an uncharged
failed_processing row marked retryable, and the row names the lever: re-run it with more run memory, which on this platform also buys more CPU.
- A stopped extraction can no longer leave a half-finished audio track behind, so a partial transcript can never be delivered or billed as a whole one.
Nothing about short creatives changed, and no failed row was ever charged.
Follow-up in the same wave: the first cut of this change computed the new time budget as a fraction of a millisecond, which the underlying process launcher refuses. Only durations between the fixed lower and upper bounds could produce a fraction, so the very short and the very long creatives were unaffected — but a creative in the middle of the range came back as a download failure, and its media was re-downloaded twice more before the run gave up. The budget is now always a whole number, and anything that stops the extraction from starting is reported as an extraction problem rather than a network one: one attempt, one honest uncharged row.
A field you left unset can now be sent as null — the run starts and uses the default.
If you call this actor from a template — n8n, an agent framework, a chained workflow — the tool usually fills in every field it knows about and writes null into the ones you left blank. Until this build the platform refused those runs before they started, with an error like Field input.maxItems must be integer. Nothing ran, nothing was charged, and the fix was non-obvious: you had to delete the key entirely rather than leave it empty.
Every optional field now accepts an explicit null and reads it as "use the default" — identical to leaving the field out. {"accountOwners": ["HubSpot"], "countries": null, "dateOption": null, "maxItems": null} runs exactly like {"accountOwners": ["HubSpot"]}. This matters more here than on most actors because the Market filters are a complete target on their own: a null country or date is read as no filter, so a template can never turn an unset field into a whole-market browse you did not ask for — and did not expect to pay for.
The Date range dropdown needed more than the other fields: a dropdown is validated against its list of allowed values and null is on no list, so it keeps its options and titles in the input form while the run itself now checks the value. A date option that is not on the list is no longer refused before the run; it is caught by the run instead, which returns an uncharged row naming what it did not recognise and leaves that filter off rather than guessing — the same way this actor already answers an unusable country code or date. Nothing about what this actor delivers, or charges, changed.
The opening hook is never blank again, and it now tells you when it starts.
hook3s is the first 3 seconds of speech in an ad. Plenty of LinkedIn creatives — brand films, long branded title cards, anything that opens on music — say nothing at all in those 3 seconds, and until now those ads came back with the hook column empty: exactly what you would see if the actor had simply failed to get the line. On a charged row, that was our headline field arriving blank.
- A late opening line is delivered, not dropped. When nothing is spoken in the ad's first 3 seconds,
hook3s now carries the first 3 seconds of speech from wherever speech actually begins — the real opening line.
- New
hookStartSeconds column. The second that line starts, so a late hook can never be mistaken for one that opened the ad. 0 means the ad opens speaking. Sort by it to see which competitors make you wait for the pitch.
- No more blank strings. An ad with no speech at all leaves both fields empty (
null), so "this ad opens silent" and "we lost the line" can no longer look identical in your spreadsheet.
Nothing about what this actor delivers otherwise, or charges, changed.
Search a market, not just a name — and browse one with no keyword at all. A new Market filters section brings across the Ad Library's own filters: country, date range (last 30 days, this month, this year, last year, or your own start and end dates), payer, and a minimum/maximum impressions range. Use them beside an advertiser or keyword to narrow a search — or set them on their own, leave the advertiser and keyword fields empty, and the run browses that market's recent ads and returns them as transcripts and image text. That is the fastest way in when you do not yet know which advertisers or phrases to look for. LinkedIn applies these filters on its side, so a filtered search comes back with a genuinely different set of ads rather than a trimmed one. Countries are searched one at a time because that is all LinkedIn accepts, so every country you list is its own search and counts against Max ads to process and your max charge, exactly as extra keywords do. A run with no advertiser, no keyword, no link, no dataset and no filter is still an error, as before. One honest limit worth knowing: LinkedIn publishes impression counts only for ads shown in the EU, so filtering on impressions also limits results to EU-served ads.
Paste an Ad Library search link and the run inherits every filter in it. A linkedin.com/ad-library/search?… link — the address bar after you filter the library by hand in your browser — used to be turned away as "a LinkedIn page". Drop it into Ad URLs, video URLs or search links and it runs as that search, filters included. LinkedIn links that genuinely are not searches are still refused, with the same explanation as before.
An empty filtered search now finishes straight away. When a filter legitimately matches nothing, LinkedIn answers with a page that still looks as if more pages follow; the run used to keep asking for them. It never changed what was delivered or charged — only how long an empty search took.
Prices, and what is and is not charged, are unchanged.
A run now spends the max charge you set. When several ads were being processed at once, an ad that turned out to carry no speech released the charge it had reserved — but by then the ads still waiting had already been answered with skipped_budget rows, so that released budget had nobody left to spend it and a run could finish having delivered nothing while charging nothing. Ads now wait for budget that is still in play instead of being skipped past it, so a run delivers as many results as your max charge allows, and maxItems still counts exactly as before. Some ads failed to download with a network error no re-run could fix — the video address the run reached for was not reachable from it, and every retry reached for the same one. Video and image downloads now take a route the run can always reach, so those ads come back as transcripts instead of "please re-run" rows. Nothing about what is delivered or charged changed.
The long-video surcharge is now spelled out in the input form. The Max ads to process field's description now says what the Pricing tab and this page already did: the first 3 minutes of every video are included in the transcript price, and each started minute beyond that is charged as a "Long-video minute (surcharge)" at $0.005 — only on delivered transcripts, never on image ads or on-screen text. The output table on this page now says the same next to its "any length" promise. Nothing about what is delivered or charged changed.
The output description now names both kinds of charged row. It still said only transcribed rows are charged, from before image ads had their on-image copy read. Image reading is on by default, and a delivered image_text_extracted row is charged at the same price as a transcript — the Pricing tab and the option's own description already said so, and now the output description does too. Nothing about what is delivered or charged changed.
Every ad the run found is now accounted for. When your max charge stops a run before every queued ad is processed, each unprocessed ad ships as its own uncharged skipped_budget row — advertiser, format, ad ID and Ad Library link included, marked retryable: true — so you can raise the cap or chain the links next time instead of reading a bare count. The status line now reconciles the whole queue (ads found, search results folded because they reuse one creative, processed, left) and names the cap that stopped the run — your max charge amount, or maxItems. Nothing about what is delivered or charged changed.
- README:
availability, impressions and impressionsByCountry are LinkedIn's EU ad-transparency fields — populated only on ads shown in the EU, null on US-only campaigns.
An ad with no real speech is never charged. A transcript that is only the speech engine's own filler — including the subtitle-credit lines it sometimes invents on music-only ads — one whose timing does not fit the audio, or one in a language that cannot belong to the single country the ad's own impressions name, now ships as an uncharged no-speech row. Genuinely spoken ads, in any language, are delivered and charged exactly as before.
Leaner on memory, faster on silent ads — nothing about what this actor delivers or charges changed.
- Long or large video ads no longer risk running the actor out of memory. A downloaded video is now handled as a file rather than held in memory while its audio is extracted, so a run's memory use no longer grows with the size of the creatives in it — batches of long ads stay well inside the run's memory allowance.
- Reading a silent ad's on-screen text is faster. With "Read on-screen text from silent ads" turned on, the five-frame filmstrip is now produced in a single pass instead of one pass per frame, so silent ads come back sooner. The frames sampled, the filmstrip's layout and the charge for a read ad are exactly as before.
- Every temporary file a run creates is removed the moment it is no longer needed, on every outcome — delivered, skipped, or failed.
- A momentarily busy transcription service no longer costs you the ad. When the speech-to-text service asks this actor to wait a moment, the run now waits exactly as long as it was asked (never more than a minute) before trying the same audio again, instead of retrying on a fixed short timer that could run out before the service was ready. Ads that used to come back as an uncharged "please re-run" row during a busy spell now deliver on the first run. Nothing about what this actor charges changed.
Suite links now point at the full live shelf — every actor named in this README is a live
store link. Nothing about what this actor delivers or charges changed.
- A row can never say it was charged when the charge could not land. When a run reaches its maximum cost just as a result becomes ready, that result now ships as an uncharged
skipped_budget row telling you to raise "Maximum cost per run" and re-run for it — and the rest of the run stops buying media it can no longer bill.
- An interrupted charge can no longer fail a resumed run. If a charge left over from an interrupted run cannot be completed the moment the run resumes, the run carries on and settles it later. Nothing is ever charged twice.
- "Max items" stays exact across a platform interruption. A run the platform moves to another server mid-flight now counts precisely how many items it had already attempted, so the resumed half delivers exactly what is left under the cap — never a few too many, never a few short.
- During a transcription-service outage, ads that never needed that service keep their real answer. A document or carousel creative, an input pointer, or an image ad with image reading switched off now ships its usual uncharged row instead of a "please re-run later" note that a re-run could not improve.
- A chained dataset is read in full, and a cut is always announced. Chained datasets larger than one page no longer lose rows beyond the first page: everything up to the 10,000-row ceiling is read, and a dataset holding more than that gets one uncharged row saying how many rows this run processed and how to get the remainder.
- A safety stop against ever billing a delivery twice. If this actor's pricing were ever misconfigured in a way that would bill rows promised as carrying no result fee, a run now refuses to start, says plainly the problem is on our side, and charges nothing.
- No ad disappears when our transcription service goes down mid-run. If the speech and text-reading service refuses the actor's access partway through a run, every ad still in the queue now comes back as its own row — status
asr_unavailable, retryable: true, never charged — saying the problem is on our side and asking you to re-run later. Before, those ads were only counted in the end-of-run summary, so a run (or an actor chained to its dataset) saw fewer rows than ads with no per-ad trace of why. The summary line is unchanged; the run still stops immediately without spending anything on calls that cannot succeed.
- Clearer wording on uncharged no-speech rows: an ad whose audio turns out to be music-only or silent now says exactly that in
statusReason, in plain language. The same ads are recognised as before, and they are still never charged.
This actor now reads image ads, not just video ads — and that changes what a run costs.
- Image ads get their on-image copy read and delivered as a new
imageText field (headline, body, CTA and the full verbatim rawText). About 4 in 5 LinkedIn ads are static image ads, and until now every one of them came back as an empty metadata row. Non-English copy is read as written and never translated. The advertiser name always comes from LinkedIn's own metadata, never from reading a logo in the picture.
- What this does to your bill: a keyword run now charges for roughly ten times as many rows. An image-ad extraction costs the same as a video transcript (same event, same price) and never adds a long-video surcharge. Use
maxItems and the run's max total charge to size a run — both stop it honestly with a "what's left" message. Prefer the old behaviour? Turn Read the text on image ads off and the run is video-only, exactly as before.
- Image ads cost no extra LinkedIn requests. The creative image link is already in the search result this run has, so nothing extra is loaded to find it.
- An image ad with no readable text is never charged — a plain stock photo comes back as an uncharged
ocr_no_text_found row with its metadata.
- New optional switch: read the on-screen text of silent video ads. About half of LinkedIn's video ads are silent or music-only and carry their whole message as on-screen type. Turned on, those ads' own frames are read and delivered as
onScreenText, charged like a transcript; silent ads that are just b-roll stay uncharged. It is off by default, so a silent ad is still never charged unless you ask for this.
- The run summary now says what the search saw and did not read. It names every creative format found — document, carousel, article, message — instead of dropping ~9 in 10 of them without a word.
ctaText is now a visible column. It was always in the data and missing from the results table.
- Chained rows: most creative image links never expire, but a small share are short-signed and can go stale in an older dataset — those come back as an uncharged
image_expired row telling you to re-run your scraper or search here instead. Ad video links are unaffected; they still do not expire.
maxItems now counts image ads as well as video ads, and its label says so.
A video ad we could not read the source from is no longer reported as a non-video creative.
- A video ad whose page arrives without a usable video source is now an uncharged, retryable miss that asks you to re-run it. It used to be reported as a non-video creative — both wrong and final, since LinkedIn had already identified the ad as video.
- Those pages are now retried once on a fresh connection before the miss is reported, so a one-off bad page usually resolves inside the same run.
- Image, text and document creatives are unchanged: they still pass through as uncharged metadata rows.
- The end-of-run summary now says how many ad pages LinkedIn did not serve in a readable form, rather than naming a verification page for every one of them.
- A typo in an input field name now gets a helpful pointer instead of sample rows. If you send a field this actor does not have —
companies instead of accountOwners, say — the run now returns one uncharged row that names the field it did not recognise, names the one you meant, and shows the shape to send. Before, that run looked exactly like a run with no input at all and answered with the demo row, which made a typo look like it had worked.
- The prefilled example row in "Dataset items" is now always treated as an example. Pressing Run without replacing it returns the demo row, clearly labelled as the demo, instead of reporting the sample HubSpot ad as if you had supplied it. Leaving it in among your own pasted rows drops it and processes the rest. Nothing about it was ever charged.
- Rate-limit retries now reuse the video already in hand. When the speech service asks the actor to wait, the retry transcribes the bytes it already downloaded instead of fetching the file again — retries are faster and cheaper, and a link that stopped working mid-run can no longer be misreported as permanently dead when the real cause was a brief wait.
- A speech-service access problem now stops the run honestly. If the speech service refuses this actor's access, the remaining ads are no longer processed one by one toward calls that cannot succeed: the run stops cleanly, the status line says how many ads were left and that the problem is on our side, and none of them are charged.
- Interrupted runs now resume without double-charging. If the platform moves a run to another server mid-batch (or you resurrect a finished run), the actor now recognises every ad it already delivered: nothing is fetched, delivered, or charged a second time, and any charge the interruption cut short is settled honestly. Before, a moved or resurrected run could deliver duplicate rows and charge again for ads you already had.
- The
maxItems limit now counts the whole run, so an interrupted run can no longer process more ads than the limit promises after it resumes.
- A run's time ceiling now counts from when the run started, not from the latest restart, so an interrupted run can no longer live (and cost) longer than the ceiling promises.
- Long-video surcharge budgeting now also reserves for ads still being processed by parallel workers, so a batch of long videos near your maximum cost stops honestly instead of being cut off by the platform.
- Chaining from a mistyped or unreadable Dataset ID now returns one uncharged row naming the ID and what to fix, instead of finishing quietly with an empty result.
- An Ad Library detail link is now always treated as one. A correctly formed
linkedin.com/ad-library/detail/<id> URL whose id happened to be short was turned away with
"this is a LinkedIn page, not an Ad Library detail page" — which was simply wrong about your
input. The ad is now looked up, and if LinkedIn has no such ad you get the honest uncharged row
naming the id instead of a complaint about the link.
- Rows for an ad that does not exist now name the id in question, so a long input list is easy
to audit.
- The no-input demo row now says it is a demo. It previously came back looking like an
ordinary result for a real advertiser.
sourceIndex is now unique across a whole run that mixes input types. Rows coming from a chained dataset used to restart their numbering at 0 and collide with rows from pasted video URLs, so joining results back to your input could line up with the wrong ad. Runs that use a chained dataset on its own are unchanged.
- A video download that ends early is now caught and retried instead of being transcribed. You can no longer receive — or be charged for — a transcript of half an ad; if every retry comes up short, the ad ships as an uncharged row that says so.
- A run whose maximum total charge covers exactly one transcript now returns that transcript instead of stopping with nothing.
- The budget reserve follows the new prices, so a batch of long videos stops cleanly at your maximum total charge with a "what's left" message instead of being cut off mid-run.
- Every row's
charged flag and chargeEvents now match what was actually billed for that row.
- Price cut. A delivered transcript now costs $0.020 down to $0.008 depending on your Apify plan tier — the volume price was $0.015. The free-plan price and the $0.005 long-video surcharge are unchanged, and you are still charged only when a transcript lands in your dataset.
- The pricing block in this page now shows the new figures.
- Added a plain note that this is an independent actor — unofficial, and not affiliated with or endorsed by LinkedIn.
- The directory of other steadyfetch actors now covers every family (ad creatives, trends and keywords, YouTube, Instagram, jobs, Amazon, and general media) and names each one exactly as it appears on the Store, so a link always lands where the name says. It now sits at the end of the page, just above support.
- The README now opens with an Output section — a sample results table, a screenshot of a real run's output, and the full JSON row — so you can see exactly what you get back before spending anything.
- Added a copy-and-paste block for AI agents and LLM clients: actor id, a complete input example, every output field, and the exact event prices.
- Added a directory of the other steadyfetch ad-intelligence actors (Facebook & Instagram, Google Ads, TikTok, Instagram Reels, Google Trends) with their free n8n templates.
- The example dataset linked from the README is now permanent, so it will not expire.
- Added a ready-made n8n workflow and a live example dataset to the README.
- Billing accuracy: runs with many long videos now stay within
maxTotalChargeUsd and stop honestly; surcharge minutes on each row always match what was charged; the same ad supplied in different forms (video URL, detail URL, or id) is delivered and charged once; improved music-only detection (still never charged).
- Reliability: stricter download timeouts and size safeguards; a search stopped by the time limit says so instead of reporting "no video ads found"; malformed characters in advertiser text can no longer fail a run; sturdier transcription timeouts.
- Initial release: transcripts + first-3-seconds hooks for LinkedIn Ad Library video ads — search by advertiser name or keyword (the actor finds the video ads for you), pass ad detail URLs / ad IDs / video URLs, or chain any LinkedIn Ad Library scraper run (dataset ID or pasted rows).
- Charged only on delivery: image and text creatives, silent videos, music-only audio, removed ads, and pages LinkedIn won't serve arrive as uncharged rows with the reason stated. The first 3 minutes of each video are included; longer videos add a small per-started-minute surcharge.
- Every row carries advertiser, headline, ad text, paying entity, and — where LinkedIn shows them — the ad's run dates and estimated impressions with the per-country split.