<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ivan Solovyev</title>
    <description>The latest articles on DEV Community by Ivan Solovyev (@isolovyev).</description>
    <link>https://dev.to/isolovyev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066897%2Fc493b5de-c94d-4865-9375-90b9c2685808.png</url>
      <title>DEV Community: Ivan Solovyev</title>
      <link>https://dev.to/isolovyev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isolovyev"/>
    <language>en</language>
    <item>
      <title>Two Actors, one agent, and the three ways my chain broke</title>
      <dc:creator>Ivan Solovyev</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:09:31 +0000</pubDate>
      <link>https://dev.to/apify/two-actors-one-agent-and-the-three-ways-my-chain-broke-3p38</link>
      <guid>https://dev.to/apify/two-actors-one-agent-and-the-three-ways-my-chain-broke-3p38</guid>
      <description>&lt;p&gt;A client asked a question that sounds simple: which sellers on the marketplaces are risky to buy from? Answering it takes two steps. Find the sellers behind the product listings, then check each seller's legal entity against public risk registries.&lt;/p&gt;

&lt;p&gt;I had an Actor for each half already. &lt;a href="https://apify.com/isolovyev/marketplace-seller-leads" rel="noopener noreferrer"&gt;One&lt;/a&gt; walks marketplace search results and returns sellers with their tax ID, legal name and store URL. &lt;a href="https://apify.com/isolovyev/ru-counterparty-check" rel="noopener noreferrer"&gt;The other&lt;/a&gt; takes tax IDs and returns a risk score built from bankruptcy filings, tax arrears, the procurement blacklist and the financial monitoring list. Both had been in the Store for months, both had paying users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmpew5ok8cg8az5x8okj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqmpew5ok8cg8az5x8okj.jpg" alt="Run history of the marketplace seller lead-generation Actor in Apify Console, with runs returning 15, 98, 68, 100, 50 and 40 results" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The first Actor, doing its half of the job. Each of these runs produces the input for the second one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Handing the chain to an agent broke it three times, and none of the breaks looked like a failure. The runs stayed green. The dataset filled up. The answer was wrong in a way that only showed up when I checked a company by hand.&lt;/p&gt;

&lt;p&gt;The agent reached both Actors through the &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt;, which exposes them as tools over the &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;. Nothing below depends on which client you use: the failures come from how the two Actors hand data to each other, and a human doing the same handoff by hand would have hit two of the three.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this is two Actors and not one
&lt;/h2&gt;

&lt;p&gt;The scraper needs a browser, residential proxies and a container image heavy enough to carry them. The risk check needs plain HTTP requests to four public sources and nothing else. Merged into one Actor, every risk lookup would drag a browser image behind it.&lt;/p&gt;

&lt;p&gt;They also run on different clocks. Seller lists get refreshed when a client enters a new category. Risk data gets rechecked weekly for the same sellers, because bankruptcy filings appear on their own schedule. Splitting them means the expensive half runs once and the cheap half runs as often as the client wants.&lt;/p&gt;

&lt;p&gt;That split is the right call for the platform and the wrong shape for a naive agent, which will treat the two as one tool and wire them together the first way that looks plausible.&lt;/p&gt;
&lt;h2&gt;
  
  
  Break one: the handoff by reference
&lt;/h2&gt;

&lt;p&gt;The obvious wiring is to pass the first run's dataset id to the second Actor. My builder Actors accept &lt;code&gt;sourceDatasetId&lt;/code&gt; for exactly that. It works on a laptop, where your personal token reads everything you own.&lt;/p&gt;

&lt;p&gt;On the platform, an Actor running with standard permissions cannot read the default dataset of a different run. The API answers &lt;code&gt;Insufficient permissions&lt;/code&gt;. I confirmed this on a cloud run on 25 July after losing most of a day to it locally, where &lt;a href="https://github.com/apify/apify-sdk-python" rel="noopener noreferrer"&gt;the Python SDK&lt;/a&gt; happily used my personal token and hid the whole problem.&lt;/p&gt;

&lt;p&gt;The fix is to pass values, and the part specific to chaining is this: your input schema decides which wiring the agent tries first. If &lt;code&gt;sourceDatasetId&lt;/code&gt; sits at the top of the schema with a confident description, that is the path the agent picks. I moved &lt;code&gt;items&lt;/code&gt; to the top, described it as the primary input, and demoted the dataset id to a fallback with an explicit note about permissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Companies to check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Records with an inn field, passed by value. Use this when chaining from another Actor: a run cannot read another run's dataset with standard permissions."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceDatasetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Source dataset (same-account only)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read companies from a dataset your token owns. Fails with Insufficient permissions across runs."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schema order is documentation for a reader who stops reading after the first field that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break two: the first Actor's output is the second one's input, and mine did not fit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3oo1n26efn6hbca6b9ig.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3oo1n26efn6hbca6b9ig.jpg" alt="Apify Store page of the counterparty due diligence Actor, which takes tax IDs and returns a risk score from public registries" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The second Actor has no idea where its tax IDs come from. That indifference is what makes it reusable, and what makes the handoff fragile.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The seller scraper returns a tax ID for most sellers, not all. On Wildberries the ID lives in a separate dossier request that I make only when &lt;code&gt;fetchDossier&lt;/code&gt; is on, and some storefronts have no legal entity attached at all. My run log said it plainly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;with_inn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sellers&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sellers: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sellers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (with tax ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;with_inn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, by platform: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scraped_counts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent never sees the log. It saw a list of sellers, mapped it to the risk checker's input, and passed the whole thing. Records without a tax ID went in as empty strings. The risk Actor did what it should: no company found, no risk data, one record out per record in.&lt;/p&gt;

&lt;p&gt;The result read as "these eleven sellers came back clean." They had never been checked.&lt;/p&gt;

&lt;p&gt;Two changes fixed it. First, the seller Actor now marks each record with what a downstream step can do with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;seller&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;seller&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkableReason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no legal entity published by the marketplace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, the risk Actor refuses to invent an answer for input it cannot use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;inn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;inn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_data&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sellerId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lookupStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipped_no_inn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;riskScore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# not zero: absence of a check, not absence of risk
&lt;/span&gt;    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;riskScore: None&lt;/code&gt; is the important part. A zero score means four sources answered and found nothing. &lt;code&gt;None&lt;/code&gt; with a status means nobody looked. An agent summarising fifty sellers will average those numbers, and a zero in that average is a claim I never made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break three: a schema rejection ate the results of a paid run
&lt;/h2&gt;

&lt;p&gt;This one is my favourite, because everything about it was working as designed.&lt;/p&gt;

&lt;p&gt;My risk Actor labels each risk factor with its source. The dataset schema declared the allowed values as an enum, which is good practice: it keeps the output honest and gives the Store a clean field list. The enum listed &lt;code&gt;fedresurs&lt;/code&gt; and &lt;code&gt;nalog&lt;/code&gt;, the two sources I had at the time.&lt;/p&gt;

&lt;p&gt;Then I added the procurement blacklist as a third source, labelled &lt;code&gt;rnp&lt;/code&gt;. Company records that carried a blacklist factor now failed validation on write. &lt;code&gt;push_data&lt;/code&gt; raised &lt;code&gt;Schema validation failed&lt;/code&gt;, my handler logged a warning, and the run continued and finished green.&lt;/p&gt;

&lt;p&gt;Read that again in the context of a chain. The scraper ran and charged. The risk Actor ran and charged. The most interesting companies in the batch, the ones actually on a blacklist, were the exact records that never reached the dataset. The agent received a shorter list with no indication that anything was missing, and every company on it looked fine.&lt;/p&gt;

&lt;p&gt;The fix took one line in the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"riskFactors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"fedresurs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nalog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rnp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rosfinmonitoring"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I shipped it as build 0.1.16, then verified against three tax IDs on the platform rather than locally: run &lt;code&gt;b1J1ULvJJuVL7beZd&lt;/code&gt; came back with all three non-empty, including &lt;code&gt;7536176707&lt;/code&gt; with 23 blacklist records and a risk score of 90. When I later added the financial monitoring list I extended the enum in the same commit as the parser, before any run could produce the new value.&lt;/p&gt;

&lt;p&gt;Two habits came out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat a write failure as a run failure. A warning in the log is invisible to the caller, and in a chain the caller is a machine that already paid for the data.&lt;/li&gt;
&lt;li&gt;Extend output enums in the same commit that produces the new value. There is no version of this where the parser lands first and the schema catches up later.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# the full text matters: the platform names the field that failed validation,
&lt;/span&gt;    &lt;span class="c1"&gt;# and without it a chain silently loses its most interesting records
&lt;/span&gt;    &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;push_data failed (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the agent can actually hold
&lt;/h2&gt;

&lt;p&gt;A category sweep returns a few hundred sellers. The full risk record for one company runs to dozens of fields: bankruptcy cases, arrears by year, directors, encumbrances, appeals. Multiply that out and no agent can keep the result in context, so it starts summarising, and the summary is where accuracy goes to die.&lt;/p&gt;

&lt;p&gt;I added a compact mode that returns one row per company with the decision fields only:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tax ID that was checked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;riskScore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0-100, or &lt;code&gt;null&lt;/code&gt; when no check happened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;riskLevel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;low, medium, high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;topFactors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;up to three factor codes that drove the score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lookupStatus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ok, not_found, source_unreachable, skipped_no_inn&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full dossiers stay in the dataset for a human to open. The agent gets five fields per company, which fits, and it can request the full record for the handful that matter. My prefill company, tax ID &lt;code&gt;7707083893&lt;/code&gt;, comes back as &lt;code&gt;riskScore: 35, riskLevel: medium&lt;/code&gt;, which is one line instead of six screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paying twice for the same answer
&lt;/h2&gt;

&lt;p&gt;Chained runs invite repetition. An agent that gets a partial result retries the step, and a retry of step two is a second charge for companies that were already checked in the first attempt. My billing is one event per company, so a fifty-seller batch retried once costs a hundred events for fifty answers.&lt;/p&gt;

&lt;p&gt;The cheap defence is to make repetition visible before it happens. The risk Actor now drops duplicate tax IDs inside a single input, reports how many it dropped, and refuses to charge for a company it already answered in the same run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deduped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate inputs dropped: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across runs the responsibility shifts to the caller, and the caller is an agent with no memory of yesterday. What helps there is making the record self-dating: every row carries &lt;code&gt;checkedAt&lt;/code&gt;, so an agent that keeps its own store can decide whether a seven-day-old risk score needs refreshing before it spends money on one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently from the start
&lt;/h2&gt;

&lt;p&gt;I built both Actors for a person clicking Start in the console, then adapted them for a chain. Building for the chain first would have changed four decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every record states whether the next step can use it, and why not when it cannot.&lt;/li&gt;
&lt;li&gt;Absence of a check and absence of a finding are different values, never both &lt;code&gt;0&lt;/code&gt; or both &lt;code&gt;[]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A failed write ends the run instead of shortening the dataset.&lt;/li&gt;
&lt;li&gt;There is a compact output shape, because the second consumer of your data has a context limit and the first one had a monitor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The chain works now. An agent runs the seller sweep, passes the checkable records by value, gets five fields back per company, and asks for full dossiers only where the score crosses a threshold. What made it work was not new scraping code. It was making both Actors honest about what they did not do.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>The last mile of a scraper is a human copying a spreadsheet</title>
      <dc:creator>Ivan Solovyev</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:07:05 +0000</pubDate>
      <link>https://dev.to/apify/the-last-mile-of-a-scraper-is-a-human-copying-a-spreadsheet-48oo</link>
      <guid>https://dev.to/apify/the-last-mile-of-a-scraper-is-a-human-copying-a-spreadsheet-48oo</guid>
      <description>&lt;p&gt;I scrape product cards from online stores for a content bank with &lt;a href="https://apify.com/isolovyev/ru-product-cards" rel="noopener noreferrer"&gt;an Actor of my own&lt;/a&gt;. The Actor finishes, the dataset fills up, and then a person exports it, checks it, and pastes it into the tool where the work actually happens. That last step has no technology in it. It is somebody's Tuesday.&lt;/p&gt;

&lt;p&gt;When Apify shipped &lt;a href="https://docs.apify.com/platform/integrations/mcp-connectors" rel="noopener noreferrer"&gt;MCP connectors&lt;/a&gt;, I wanted to delete that step. A connector lets an Actor call a third-party service over the &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; using credentials the user authorized once, on the platform. The Actor never holds the token. It talks to an Apify proxy with its own run token, and the platform injects the real credentials server-side.&lt;/p&gt;

&lt;p&gt;I built a small Actor to try it: take product cards, write them into whatever service the user connected. The full code is at &lt;a href="https://github.com/isolovyev77/apify-card-sink" rel="noopener noreferrer"&gt;github.com/isolovyev77/apify-card-sink&lt;/a&gt;. It works now. Getting there took six failed runs, and every one of them failed differently.&lt;/p&gt;

&lt;p&gt;Versions matter in this article more than usual, because one of the failures is purely a version mismatch. Mine: &lt;code&gt;apify==3.4.1&lt;/code&gt;, &lt;code&gt;mcp&amp;gt;=1.2.0&lt;/code&gt;, &lt;code&gt;httpx==0.28.1&lt;/code&gt;, on the &lt;code&gt;apify/actor-python:3.12&lt;/code&gt; base image. The platform reported the run as SDK 3.4.1, client 2.5.1, Crawlee 1.9.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Actor does
&lt;/h2&gt;

&lt;p&gt;One job: take cards by value, connect to the service the user picked, find a tool that can write, write the rows, and report what happened.&lt;/p&gt;

&lt;p&gt;The design decision worth explaining is that the Actor does not know its destination. It does not have a Notion branch and a Supabase branch. It asks the connected service which tools it exposes, picks one that can write, reads the argument schema that service published, and shapes the call to match. Connect it to a Notion workspace and it creates pages. Connect it to a database and it inserts rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declaring the connector
&lt;/h2&gt;

&lt;p&gt;A connector is an input field with &lt;code&gt;resourceType: "mcpConnector"&lt;/code&gt;, which the &lt;a href="https://docs.apify.com/platform/integrations/mcp-connectors/use-in-actors" rel="noopener noreferrer"&gt;Actor-side guide&lt;/a&gt; documents in full. The &lt;code&gt;mcpServers&lt;/code&gt; list does two jobs at once: it filters which of the user's connectors show up in the picker, and it caps what the Actor may call at runtime. The proxy holds you to the declaration, so a tool you did not declare is not just discouraged, it is unreachable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outputConnector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Destination"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Where to write the cards. Pick a connector you have authorized: a Notion workspace, a Supabase project, a Slack channel. Your credentials stay on the Apify side and never reach this Actor."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcpConnector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resourcePicker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"insert*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"create_*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"post_*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Apify Console the field renders as a picker, and the connector the user chose is what the&lt;br&gt;
run receives:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrk4ooc1tcfafph2e6j2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyrk4ooc1tcfafph2e6j2.jpg" alt="The Actor input form in Apify Console, showing a Destination field with the Notion connector selected" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The &lt;code&gt;mcpConnector&lt;/code&gt; field as the user sees it. No token, no endpoint, just a choice.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That declaration is where my first real mistake lives, and I will come back to it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Six runs, six different failures
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5lpmid17fzazfo0ahoxg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5lpmid17fzazfo0ahoxg.jpg" alt="Run history of the Actor in Apify Console: nine runs, four succeeded with one result each, two failed with an exception" width="800" height="426"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Nine runs in one night. The two red ones are where the MCP client crashed before reaching Notion.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run one lasted three seconds and produced nothing.&lt;/strong&gt; Exit code 0, empty dataset, no error anywhere. I had written &lt;code&gt;async def main()&lt;/code&gt; and never called it. The module imported cleanly, defined a function, and exited. On a platform that reports success by exit code, forgetting &lt;code&gt;asyncio.run(main())&lt;/code&gt; looks exactly like an Actor with nothing to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run two also produced an empty dataset&lt;/strong&gt;, this time correctly: no connector was selected, so the Actor logged a warning and returned. The behaviour was right and the reporting was wrong. I had just written an article arguing that a caller sees the dataset and never the log, and here I was, putting the one fact the caller needed into the log. Now every refusal is a row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_data&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;connector_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;refuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_connector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no connector selected: nowhere to write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run three crashed inside the MCP client&lt;/strong&gt;, before a single byte reached Notion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: not enough values to unpack (expected 3, got 2)
  File "/usr/src/app/src/main.py", line 102, in main
    http_client=http_client) as (read, write, _):
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python example in the Apify documentation unpacks three streams from &lt;code&gt;streamable_http_client&lt;/code&gt;. The version of the MCP SDK that installed in my image yields two. Both are correct for their own version; a fixed unpack is what breaks. Index the result instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;streamable_http_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;connector_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="n"&gt;http_client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;http_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run four connected, and the tool matcher came up empty.&lt;/strong&gt; My write hints were written for the names I imagined: &lt;code&gt;create_page&lt;/code&gt;, &lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;append&lt;/code&gt;. Notion exposes &lt;code&gt;notion-create-pages&lt;/code&gt;. Different word, different separator. The naive repair is to match on &lt;code&gt;create&lt;/code&gt;, which then also matches &lt;code&gt;notion-create-attachment&lt;/code&gt; and &lt;code&gt;notion-create-file-upload&lt;/code&gt;, and product cards do not belong in either. You can see the full set the connector exposes in Console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdp3uuhebnylk4mmzeple.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdp3uuhebnylk4mmzeple.jpg" alt="The MCP connectors section in Apify Console settings, listing 27 Notion tools including notion-create-pages and notion-update-page" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Twenty-seven tools, every name hyphenated. This screen is also where you limit what an Actor may call.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The list is explicit now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;WRITE_HINTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create-pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create-page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add_row&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_sql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;post_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is also why the &lt;code&gt;mcpServers&lt;/code&gt; declaration above is wrong in a way you will not notice until a connector fails to appear in the picker. Patterns like &lt;code&gt;create_*&lt;/code&gt; never match &lt;code&gt;notion-create-pages&lt;/code&gt;. While I was learning what services actually name their tools, I widened the declaration to &lt;code&gt;[{"url": "*"}]&lt;/code&gt; and let the code do the filtering. Narrow it back once you know the names you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run five wrote nothing because I could not see the argument shape.&lt;/strong&gt; My dry run reported the tool it would use and a &lt;code&gt;null&lt;/code&gt; where the schema should be. &lt;code&gt;getattr(tool, "inputSchema", None)&lt;/code&gt; returned an object that did not survive the JSON dump. The SDK hands back pydantic models, and the field is &lt;code&gt;input_schema&lt;/code&gt; in a dump, not &lt;code&gt;inputSchema&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Argument shape of a tool, in a form that survives a JSON dump.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_dump&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;callable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_dump&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shape unavailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that fixed, the dry run returned all 27 tools the Notion connector exposes and the full schema of the one it picked. That output is the single most useful thing this Actor produces, which is why &lt;code&gt;dryRun&lt;/code&gt; is a first-class input rather than a debug flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is no universal write call
&lt;/h2&gt;

&lt;p&gt;Reading that schema killed my original design. I had assumed a write is a write: hand the tool a table name and a list of rows. Notion wants pages, each with a title property and a Markdown body. A database wants rows. A chat wants a channel and a text blob.&lt;/p&gt;

&lt;p&gt;So the Actor stopped guessing from the tool name and started reading the published schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_arguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Shape the call for the tool we picked.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;schema_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Untitled product card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;as_markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notion-style pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%s - %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                           &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unrecognised argument shape, sending our own&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last branch matters as much as the first three. When the shape is unfamiliar, the Actor says so in the dataset instead of sending a hopeful payload into somebody's workspace.&lt;/p&gt;

&lt;p&gt;One detail from Notion's schema saved me a whole feature: the parent is optional. Without it, created pages land as private workspace-level pages. I had been about to build a parent-search step, and the service documentation had already answered it.&lt;/p&gt;

&lt;p&gt;Run six wrote for real. One card in, one page out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"delivered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notion-create-pages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"argumentShape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notion-style pages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;pages&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;3b3b52ac-9795-818b-b31c-fcc9852969ce&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;properties&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Lenovo IdeaPad Slim 3 15ABR8&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}}]}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6t4pxqkdl9d1tgh99dpe.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6t4pxqkdl9d1tgh99dpe.jpg" alt="Apify Console log of the successful run, showing 27 tools discovered, the notion-create-pages call and the response with the created page id" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The whole exchange in five log lines: connect, list tools, pick one, call it, get a page id back.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fily88zawxrhnibycvu8s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fily88zawxrhnibycvu8s.jpg" alt="Apify dataset view of the run, one row with delivered 1, status ok, tool notion-create-pages" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The dataset carries what happened, including which argument shape was used.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The page appeared in the workspace with the product title as its heading and the card body in Markdown underneath.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj21q5bgfw23g8kd11u2f.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj21q5bgfw23g8kd11u2f.jpg" alt="The resulting Notion page titled Lenovo IdeaPad Slim 3 15ABR8, with platform, price, specification count, image count and a link to the product" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The end of the last mile: a card that arrived without anyone exporting anything.&lt;/em&gt; Total time from the Actor starting to the page existing: under ten seconds, and my Actor never saw a Notion token.&lt;/p&gt;
&lt;h2&gt;
  
  
  The connector I could not use
&lt;/h2&gt;

&lt;p&gt;I started with Supabase, because I already run one. Apify accepted the server URL, then told me the server does not support dynamic client registration and recommended registering my own OAuth application. Notion, by contrast, has managed OAuth: pick it from the dropdown, authorize, done.&lt;/p&gt;

&lt;p&gt;This is worth checking before you design around a service. Apify provides managed OAuth for Notion and Supabase; for GitHub, Slack, Google and others you bring your own OAuth client. Supabase also accepts a personal access token through the API key method, which is the shortcut if you need that one specifically.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the security model actually buys you
&lt;/h2&gt;

&lt;p&gt;Three layers decide what a connector-enabled Actor can do, and they compose: the scopes granted at authorization, the tool allowlist the user sets on the connector in Console, and the Actor's own &lt;code&gt;mcpServers&lt;/code&gt; declaration. The proxy filters &lt;code&gt;tools/list&lt;/code&gt; down to what the Actor declared and rejects calls outside it.&lt;/p&gt;

&lt;p&gt;For a published Actor this is the difference between "give me your API key" and "pick a connector". My Actor never sees a Notion token. A user who wants to be stricter can allow only page creation on their connector, and the Actor cannot get around it. Access dies with the run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deploying it, and a wall I hit on the way
&lt;/h2&gt;

&lt;p&gt;I deploy Actors by pushing source files through the &lt;a href="https://docs.apify.com/api/v2" rel="noopener noreferrer"&gt;Apify API&lt;/a&gt;. That stopped working here: bodies over roughly 20 KB left my machine intact and never came back with a response. Rather than fight it, I switched the Actor to build from Git.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sourceType: GIT_REPO
gitRepoUrl: https://github.com/isolovyev77/apify-card-sink#main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tiny request body sidestepped the problem entirely, and the article got a public repository as a side effect. If you are debugging an Actor through repeated deploys, this is the better default anyway: the build log tells you which commit it built.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone starting this today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Ship &lt;code&gt;dryRun&lt;/code&gt; before you ship the write path. List the tools, dump the schema, write nothing. Everything else in this article was discovered by that one code path.&lt;/li&gt;
&lt;li&gt;Declare &lt;code&gt;{"url": "*"}&lt;/code&gt; while you learn the names, then narrow. A pattern that does not match leaves the user staring at an empty picker with nothing to click.&lt;/li&gt;
&lt;li&gt;Read the tool's published schema instead of matching on its name. Names vary by service and separator; schemas do not lie.&lt;/li&gt;
&lt;li&gt;Put every refusal in the dataset. The caller sees rows, not logs, and this is doubly true when the caller is an agent.&lt;/li&gt;
&lt;li&gt;Check the authentication method for your service before you design around it. Managed OAuth and "register your own OAuth app" are half an hour apart.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last mile is closed now. Cards land where the work happens, and nobody exports a spreadsheet on Tuesday.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>mcp</category>
      <category>automation</category>
      <category>python</category>
    </item>
    <item>
      <title>My Actors worked fine until an AI agent called them</title>
      <dc:creator>Ivan Solovyev</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:12:18 +0000</pubDate>
      <link>https://dev.to/apify/my-actors-worked-fine-until-an-ai-agent-called-them-4b2k</link>
      <guid>https://dev.to/apify/my-actors-worked-fine-until-an-ai-agent-called-them-4b2k</guid>
      <description>&lt;p&gt;On 27 July I published a scraper that pulls product cards and specifications from online stores. It was my fourteenth Actor. The others cover company due diligence by tax ID, government tender feeds, cadastral records, marketplace seller leads. All of them had months of green runs behind them.&lt;/p&gt;

&lt;p&gt;Then I connected the &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt; to Claude. It exposes Actors as tools an AI client can call over the &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;. I gave it a task in plain language: find this product across those stores and return the specs.&lt;/p&gt;

&lt;p&gt;The agent picked my Actor, filled the input, started the run. Status: succeeded. Zero results, no error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxe4z6xwj76h48egdhg86.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxe4z6xwj76h48egdhg86.jpg" alt="Run history in Apify Console with the Origin column reading MCP for the most recent run" width="800" height="388"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Origin column says MCP: that run was started by an agent, not by me clicking Start.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The scraper worked. The agent had read my input schema the way any careful reader would. The trouble is that I wrote that schema for a reader who already had the target website open in another tab, and that reader was me, six months earlier.&lt;/p&gt;

&lt;p&gt;This is what I changed across fourteen Actors to make them callable by something that cannot ask me a question. Two of those changes were about input fields. The ones that cost me money were about billing, and about the gap between "nothing exists" and "I could not look."&lt;/p&gt;

&lt;p&gt;The code below is Python, using the &lt;a href="https://github.com/apify/apify-sdk-python" rel="noopener noreferrer"&gt;Apify SDK for Python&lt;/a&gt; version 3.4.1, and every snippet is lifted from an Actor that runs in production today.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why an agent breaks things a human never would
&lt;/h2&gt;

&lt;p&gt;A human who gets an empty dataset has options. They open the store page, reread the README, try another value, send me an email. The whole recovery loop takes five minutes and never reaches me.&lt;/p&gt;

&lt;p&gt;An agent has one shot inside a longer plan. It reads your schema, picks values, fires the run, reads the output, and branches. If your Actor answers six different situations with the same empty array, the agent treats all six as one fact and keeps executing with confidence. The wrong branch is now running, and nothing looks broken anywhere.&lt;/p&gt;

&lt;p&gt;So the question stopped being "is my schema documented" and became: can a caller who has never seen the target site tell my outcomes apart?&lt;/p&gt;
&lt;h2&gt;
  
  
  My input fields described the website, not the task
&lt;/h2&gt;

&lt;p&gt;The clearest case came from my software registry Actor. It queries a national software registry, which groups products into classes: &lt;code&gt;06.09&lt;/code&gt; is a class, &lt;code&gt;06&lt;/code&gt; is the group above it. Those codes are printed on the site and appear in every document a user would quote.&lt;/p&gt;

&lt;p&gt;The registry's listing endpoint ignores them. It filters by an internal option id, an opaque integer that lives only inside their markup. You learn it by loading the listing page and mapping labels to ids. My first version exposed that integer, because that is what the request needed.&lt;/p&gt;

&lt;p&gt;No caller can produce that value. An agent asked about class &lt;code&gt;06.09&lt;/code&gt; passes &lt;code&gt;06.09&lt;/code&gt;, gets a clean run and an empty dataset. An agent that guesses an integer gets a full dataset for the wrong class, which costs more, because nobody checks a result that looks right.&lt;/p&gt;

&lt;p&gt;I moved the translation inside the Actor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"softwareClasses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Software classes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stringList"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Class codes as printed on the registry site: 06.09 for one class, 06 for the whole group. The Actor resolves them to the registry's internal filter ids."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prefill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"06.09"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feouunw2x0ehmz6hehn4q.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feouunw2x0ehmz6hehn4q.jpg" alt="Input schema of the Actor as rendered on its Apify Store page, showing field descriptions for product URLs and product names" width="800" height="388"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The input as a caller sees it. Every sentence here is the only instruction an agent will ever get.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rule I now apply to every input field: if a value can only be obtained by inspecting the target site, keep it out of the schema. Resolve it in code. When you cannot, make it an &lt;code&gt;enum&lt;/code&gt; with readable titles, so the caller picks from a closed list instead of inventing. The &lt;a href="https://docs.apify.com/platform/actors/development/actor-definition/input-schema/specification/v1" rel="noopener noreferrer"&gt;input schema specification&lt;/a&gt; supports both, and the choice between them is the whole difference between a caller that guesses and one that picks.&lt;/p&gt;

&lt;p&gt;That rule caught a second field the same week. My product Actor takes a &lt;code&gt;platforms&lt;/code&gt; list, which used to be free-form strings matching my internal module names. Agents passed &lt;code&gt;Ozon&lt;/code&gt;, &lt;code&gt;ozon.ru&lt;/code&gt;, &lt;code&gt;OZON&lt;/code&gt;. My code wanted &lt;code&gt;ozon&lt;/code&gt;. It is an enum now, and unknown values get reported in the output instead of dropped.&lt;/p&gt;
&lt;h2&gt;
  
  
  Empty and failed looked identical, and that cost a client money
&lt;/h2&gt;

&lt;p&gt;The registry sits behind a certificate chain from a national CA that no default trust store carries. Requests die with &lt;code&gt;CERTIFICATE_VERIFY_FAILED&lt;/code&gt;. When I first hit that, I caught the exception, logged a warning, and returned an empty result for the company.&lt;/p&gt;

&lt;p&gt;Empty result. The same thing the Actor returns when a company is genuinely absent from the registry.&lt;/p&gt;

&lt;p&gt;For one company at a time, a human reads the warning line and moves on. My client ran two hundred tax IDs through an agent. About half the "not in the registry" answers were "the source did not answer," and both arrived as &lt;code&gt;[]&lt;/code&gt;. Those answers flowed into a decision about which suppliers to work with.&lt;/p&gt;

&lt;p&gt;Every record now carries the reason it looks the way it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;foundInRegistry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# True, False, or None when we could not check
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lookupStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_unreachable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# ok | not_found | source_unreachable
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lookupDetail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry TLS chain rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three states instead of two. &lt;code&gt;foundInRegistry: false&lt;/code&gt; claims something about the world. &lt;code&gt;null&lt;/code&gt; with a status claims something about my attempt. The agent can act on the first and retry the second.&lt;/p&gt;

&lt;p&gt;Here is the mapping I now keep for the product scraper, which has four ways to end up with fewer rows than expected:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;What the record says&lt;/th&gt;
&lt;th&gt;What the agent should do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Store returned a challenge page&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;status: blocked&lt;/code&gt;, &lt;code&gt;specCount: 0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;retry later or route to another store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search matched nothing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;status: not_found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;drop the query, do not retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page loaded, no specifications&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;status: partial&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt; present&lt;/td&gt;
&lt;td&gt;usable for links, not for a card&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store search by name unsupported&lt;/td&gt;
&lt;td&gt;&lt;code&gt;status: unsupported_mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;switch to direct URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before this table existed, all four rows were an empty array and a warning in the log the caller never sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dataset ids do not cross run boundaries
&lt;/h2&gt;

&lt;p&gt;My product pipeline is two Actors by design. &lt;a href="https://apify.com/isolovyev/ru-product-cards" rel="noopener noreferrer"&gt;The scraper&lt;/a&gt; needs a browser, proxies and a heavy image. The card builder needs one HTTP request for a category reference and no browser at all. Splitting them means the expensive half runs once and the cheap half re-runs every time a client changes the card format.&lt;/p&gt;

&lt;p&gt;I wired them the obvious way: the builder takes a &lt;code&gt;sourceDatasetId&lt;/code&gt; and reads the scraper's output. On my machine it worked on the first try.&lt;/p&gt;

&lt;p&gt;On the platform it fails. An Actor with standard permissions cannot read the default dataset of a different run, and the API answers &lt;code&gt;Insufficient permissions&lt;/code&gt;. I confirmed it on a cloud run on 25 July, after losing a day locally, because local development uses your personal token and hides the whole problem.&lt;/p&gt;

&lt;p&gt;Agents chain tools by default. Claude called the scraper, took the dataset id out of the run result, passed it to the builder, and got a permissions error it had no way to fix. The builder now takes values, and the dataset path is the fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;from_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sourceDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;from_dataset&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Works when the caller owns the dataset and supplies a token that can read it.
&lt;/span&gt;    &lt;span class="c1"&gt;# A cross-run read with the Actor's own permissions returns
&lt;/span&gt;    &lt;span class="c1"&gt;# "Insufficient permissions", so agent workflows should pass items.
&lt;/span&gt;    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apify_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_dataset&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No input records: pass items, or a dataset this run can read.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you build a family of Actors meant to compose, make values the interface and references the optimisation. The agent holds that data in its context anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Billing is part of the interface
&lt;/h2&gt;

&lt;p&gt;My scraper charges per event: one price for a card assembled from free methods, another when a paid fallback channel was required, plus a surcharge for building the publish-ready structure. The run decides which events fire based on what the store made it do.&lt;/p&gt;

&lt;p&gt;Two things went wrong, and both belong to callers who never read a pricing page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe02gsemj7tx5sz4qy9xl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe02gsemj7tx5sz4qy9xl.jpg" alt="Pricing tab of the Actor on Apify Store, listing three pay-per-event prices: basic card, premium card and ready-to-publish card" width="800" height="388"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Three events, three prices. Which ones fire is decided by the run, so an agent cannot predict the bill from the schema alone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The surcharge shipped as a default.&lt;/strong&gt; The flag that builds the ready-to-publish card started as &lt;code&gt;true&lt;/code&gt;, because that output demos well. An agent calling the Actor for specifications paid for a formatting step it never used and never mentioned. I flipped &lt;code&gt;buildCards&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; and put the price consequence in the field description. Anything that costs extra should be opt-in, priced in the text the caller actually reads.&lt;/p&gt;

&lt;p&gt;The check that catches this takes a minute. Run the Actor in the cloud, pull &lt;code&gt;chargedEventCounts&lt;/code&gt; off the run object, compare it against the events you expected. Not the local run. The billed one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I charged in the wrong order,&lt;/strong&gt; and I had a reason that sounded good. I wanted every dataset row to state what it had been billed as, so a client could reconcile an invoice line by line, and I did not want to charge for a card that failed to save. So the run pushed the row, then charged, then set &lt;code&gt;billedAs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Datasets are append-only. After &lt;code&gt;push_data&lt;/code&gt; returns, that row is frozen. Assigning &lt;code&gt;billedAs&lt;/code&gt; afterwards changed an object in my process and nothing else. Clients received &lt;code&gt;billedAs: null&lt;/code&gt; while the invoice showed charges, which killed the one feature the field existed for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Charge BEFORE the write. The reverse order avoided billing for a card that never
# landed, but a dataset row cannot be edited after the fact: billedAs assigned
# afterwards changes only the in-memory object, and the client receives an empty
# field while the invoice shows a charge.
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card_obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;to_charge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billedAs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;charge_for_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paidMethods&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;emit_cards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The general form has nothing to do with billing: compute anything the caller needs to see before the immutable write.&lt;/p&gt;

&lt;p&gt;While I was in that function I stopped charging for empty results. A page that yielded a title and no specifications is not a product card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;charge_for_card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paid_stages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build_cards&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;specCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;                      &lt;span class="c1"&gt;# no specs, no card, no charge
&lt;/span&gt;    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EVENT_PREMIUM&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;paid_stages&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;EVENT_BASIC&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;build_cards&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attributeCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EVENT_READY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# billedAs must reflect the FACT of a charge, otherwise reconciliation
&lt;/span&gt;        &lt;span class="c1"&gt;# fails in both directions
&lt;/span&gt;        &lt;span class="n"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;charge &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The platform's automated test is your first agent
&lt;/h2&gt;

&lt;p&gt;Apify runs your Actor with its prefill input on a schedule and flags it as under maintenance when the run produces nothing. The flag pulls you out of Store search until it clears.&lt;/p&gt;

&lt;p&gt;I got flagged twice, and the scraper was healthy both times.&lt;/p&gt;

&lt;p&gt;The first prefill hit four marketplaces at once. That is a realistic user request, it takes longer than the test allows, and the test gave up. My Actor sat marked as broken while paying users ran it all day. The prefill is now the cheapest useful request I have: one store, one query, under five minutes. The impressive four-store demo moved to a saved task, where nobody holds a stopwatch.&lt;/p&gt;

&lt;p&gt;The second time, &lt;a href="https://apify.com/isolovyev/ru-marketplaces-price-monitor" rel="noopener noreferrer"&gt;my marketplace monitor&lt;/a&gt; lost one store's API host and the prefill happened to use that store. The verdict was fair. What I took from it is that your prefill decides which single failure can delist you, so it should sit on your most reliable path rather than your most interesting one. I raised the per-page retry budget, redeployed as build 0.1.46, and the run came back with 200 cards in 96 seconds.&lt;/p&gt;

&lt;p&gt;Treat that test as a preview of every agent that will call you: no context, no patience, one attempt, and a verdict based on whether output appeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I run before an Actor meets an agent
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Can a caller produce every input value without opening the target site? If not, resolve it in code or use an enum.&lt;/li&gt;
&lt;li&gt;Does the output separate "no such thing" from "I could not check"? Tri-state fields plus an explicit status.&lt;/li&gt;
&lt;li&gt;If this Actor is chained with another, does data pass by value? Cross-run dataset reads fail on standard permissions.&lt;/li&gt;
&lt;li&gt;Is anything that costs money on by default? Turn it off, and state the price where the caller reads.&lt;/li&gt;
&lt;li&gt;Is every field the caller needs computed before &lt;code&gt;push_data&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Is the prefill my most reliable path, and does it finish fast?&lt;/li&gt;
&lt;li&gt;Does &lt;code&gt;chargedEventCounts&lt;/code&gt; on a real cloud run match what I believe I charge?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this made my scrapers better at scraping. It made them usable by a caller who cannot ask me anything, which turned out to be the same as making them usable by anyone other than me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx3bz8gqnj8km9445fv5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkx3bz8gqnj8km9445fv5.jpg" alt="Apify Store page of the Russian Marketplace Product Card and Specs Scraper, showing pay-per-event pricing from six dollars per thousand basic cards" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Actor most of these lessons came from. Pay-per-event pricing is what turned the billing bugs from cosmetic into expensive.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The two Actors most of this came from are &lt;a href="https://apify.com/isolovyev/ru-product-cards" rel="noopener noreferrer"&gt;the product card and specs scraper&lt;/a&gt;, which handles the scraping and the pay-per-event side, and &lt;a href="https://apify.com/isolovyev/ru-counterparty-check" rel="noopener noreferrer"&gt;the counterparty due diligence Actor&lt;/a&gt;, where an empty array first passed for a fact.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>mcp</category>
      <category>ai</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
