I have 23 audit Actors on the Apify Store. They all do a version of the same thing: take a public record, check whether what it still claims is true, and write the verdict into a dataset. Last week I set out to prove they were unsafe for AI agents to chain together. I had a specific accusation in mind, I built the experiment to demonstrate it, and the experiment refused. What I found instead was a one-line schema fix that I have now applied, and a distinction between two input-schema settings that I had treated as interchangeable for months. The accusation I was going to make Chaining means running Actor A, taking something out of its output, and passing it into Actor B. For an agent to do that through the Apify MCP server, B has to be callable with a value that only A can supply. MCP is the Model Context Protocol, the interface that exposes your Actor to clients like Claude and Cursor as a tool they can call on their own. So I read my own catalogue with a script instead of from memory. Of my 23 published Actors, 9 declare a URL or a domain in their input schema's required array. Counting every ordered pair where A emits the kind of identifier B requires, I get 61 possible chains: 36 joined by a URL, 25 by a domain. A tenth Actor requires an identifier too, and it is the interesting one. dataset-to-github-issues takes a dataset ID and a repository name. Nothing else in my catalogue emits either, so no agent can ever reach it from another Actor's output - it is a second stage with no possible first stage. I had not noticed that until the script told me. Then I looked at what those 9 do when the value never arrives. I had swept my catalogue for target-naming defaults once before and found them, so the count itself was not the surprise. Restricting it to the Actors that can actually be a second stage sharpens it: seven of the nine carry a default that names a concrete target. Actor field default bulk-domain-checker domains ["apify.com"] dead-link-checker domains ["docs.apify.com"] domain-availability-checker domains ["apify.com"] http-status-checker urls ["https://apify.com/store"] seo-ai-visibility-auditor startUrls [{"url":"https://apify.com"}] sitemap-checker domains ["apify.com"] tech-stack-detector domains ["apify.com"] All seven are also listed in required. I had assumed that word did some work. My accusation was going to be this: in a two-stage workflow, an agent that drops the value between stages gets no error. The second Actor runs on apify.com, returns a well-formed dataset, and the agent reports a confident answer about a website the user never asked about. A wrong answer with no failure attached to it. First, the part that turned out to be true Before involving any agent, I called two of my own Actors with an empty input from the command line. The first, seo-audit-tool, has urls in required and no default: apify call aiqlabs/seo-audit-tool --input '{}' Run: Calling Actor aiqlabs/seo-audit-tool (ZjU3YtyaWgqpNriNj) Error: Input is not valid: Field input.urls is required No run was created. No dataset, no compute, no partial result to misread. The platform named the missing field and stopped. The second, http-status-checker, has urls in required and a default: apify call aiqlabs/http-status-checker --input '{}' INFO Checking 1 URL(s) with 8 in flight. HTML analysis: on. INFO Done. 1 URL(s): 0 error, 0 warning, 0 info, 1 clean. Apify call result: SUCCEEDED Run ID: sMLok9a5KMSSdtnD1 I sent nothing. It audited something. Reading the input the platform stored for that run shows what: {"urls":["https://apify.com/store"],"analyzeHtml":true,"respectRobotsTxt":true, "robotsAgent":"Googlebot","onlyIssues":false,"maxConcurrency":8, "requestTimeoutSecs":20,"maxRedirects":8,"slowResponseMs":3000,"maxUrls":10000} Same platform, same account, same minute, same field name, both fields marked required. One call was refused and one was silently completed. The only difference is the default. There is a second thing in that pair worth pulling out. seo-audit-tool does carry a prefill on urls - two example URLs that appear in the Console form. It was still rejected. prefill populates the form for a human; default is substituted for whoever omits the field. The input schema specification is not vague about this. On prefill: this field is only used in the user interface but does not affect the Actor functionality and API and, in the same paragraph: the Prefill value won't be used by existing integrations such as Actor tasks or API calls, but the Default will be if specified On default, it names the callers explicitly: the platform passes the value when the user omits it "via any means (API, CLI, scheduler, or user interface)". An agent calling through the MCP server is one of those means. I had read that page. I read it as a description of two features and used both, on the same field, for months. There is a detail in http-status-checker that makes the gap concrete. The two settings on urls do not even hold the same value: "urls": { "prefill": ["https://apify.com/store", "https://apify.com/this-page-does-not-exist", "http://apify.com/"], "default": ["https://apify.com/store"] } Three URLs for the form, one URL for anyone who omits the field. I wrote both, months apart, and never put them side by side. A caller that skips urls does not get the demo I built - it gets a shorter thing I stopped thinking about, and no part of the interface ever shows the two together. Then the accusation collapsed To test the chain I needed a question that one Actor cannot answer on its own. That took two attempts. My first idea was sitemap-checker into http-status-checker: enumerate a site's sitemap, then check the URLs. It is a bad example, because sitemap-checker has checkUrlStatus set to true by default and already requests every URL. My second idea was hacker-news-link-rot into domain-availability-checker, and that fails the same way - the first Actor already emits domainRegistrationStatus. That is worth pausing on. I had built every one of these Actors to answer a question completely, which is right for a standalone Store listing and is exactly what makes them poor chain stages. Composable and self-contained are not the same design, and I had only ever aimed at one of them. The question I settled on works because it needs HTML analysis, which the sitemap Actor does not do: Among the URLs listed in crawlee.dev's sitemap, which ones are marked noindex, and which ones are soft 404s? A noindex page is one that tells search engines to keep it out of the index; a soft 404 is a missing page that returns 200 anyway. Both need the HTML of the page. sitemap-checker emits 11 fields and none of them is either. http-status-checker has both, behind an analyzeHtml switch that is on by default. The chain is necessary, and crawlee.dev is a real documentation site with a large sitemap, which makes it a fair test. I gave that question, word for word, to four fresh agents with both Actors exposed through the Apify MCP server, and no other instructions. I did not tell them to be careful, and I did not mention inputs or defaults - in an earlier experiment I learned that asking an agent to count its rows is the same as telling it where to look. Then I ignored what they told me and read what the platform stored. stage-2 run urls passed first entry p5qwOESMQB0Vwj0ls 500 https://crawlee.dev/js/api/3.11/playwright-crawler/... NPM7mW5oC848wn2ZL 174 https://crawlee.dev/js/api/3.16/puppeteer-crawler/... 3fO6vTbKnLlkDwGnw 500 https://crawlee.dev/blog H0HkakmGE2bfkAdWU 400 https://crawlee.dev/js/api/3.15/core/interface/HttpResponse 2cJHKXtsrapw9aikV 400 https://crawlee.dev/js/api/3.14/browser-pool/class/PuppeteerPlugin oGbGCrfPEM4uDuOBY 400 https://crawlee.dev/js/api/3.12/puppeteer-crawler sDQnaUb7NxX9iz88B 400 https://crawlee.dev/js/api/3.11/core/class/SystemStatus QDpb6PG685ptVthUN 400 https://crawlee.dev/blog bB20ynAk9ymZafaiY 724 https://crawlee.dev/js/api/3.14/utils/function/downloadListOfUrls Nine stage-2 runs. Every one carries real crawlee.dev URLs. Not one carries https://apify.com/store. They also batched - 174 to 724 URLs per call - rather than making one call per URL. The trap I had spent a day setting never closed. Why it never closed The mechanism is in the result once you stop arguing with it. A default stands in for a value the caller never had. Inside a chain, the agent produced that value itself one step earlier and is still holding it. The omission the default is waiting for does not occur. That reconciles this with the case where the same defect does bite. When an agent calls one of these Actors cold - no preceding step, nothing in hand - the missing field is real, the platform fills it, and the run proceeds on a target nobody chose. The danger lives at the entry point of a workflow, not in the joins. I would rather publish that than the article I planned, because it changes where you should spend attention. If you are worried about agents mangling your Actor, the risk is concentrated in the first call, not in the handoffs. What is wrong with my own experiment A reviewer would find this, so I will say it first. The question I picked has a null answer. Here is the summary record from one 500-URL batch: {"checked":500,"severity":{"ok":500,"info":0,"warning":0,"error":0}, "headline":{"broken":0,"soft404s":0,"noindexOnLivePages":0,"metaRefreshRedirects":0}} crawlee.dev has no soft 404s and no noindex pages in those URLs. "Which ones are noindex?" answers to "none". An agent that had quietly audited apify.com/store instead would probably also have answered "none". So the agents' answers cannot tell a careful run from a careless one here. Only the stored inputs can. That is the one thing I got right by accident of habit: I built the measurement on records the platform keeps, not on what the agents said about themselves. It mattered more than I expected - all four agents finished without returning a report to me at all, and every number above survived that because none of them came from an agent. A stronger version of this trial would use a site with known noindex pages, so a substituted target produces a different answer rather than the same one. If you repeat this, pick your target site for that property. The fix, and what it costs For any input field that names what to work on - a URL, a domain, a repository, an account - delete the default and keep the prefill. "urls": { "title": "URLs", "type": "array", "editor": "stringList", "prefill": ["https://apify.com/store"] } with "required": ["urls"] alongside it. The Console form still opens with an example filled in, so nothing changes for a human clicking through your Actor. What changes is that a caller who omits the field now gets Field input.urls is required instead of a clean dataset about somebody else's website. The specification anticipated this too. Its guidance for required reads: Use for fields that don't have a reasonable default and MUST be entered by the user (e.g., API token, password) Fields that don't have a reasonable default. Seven of mine had both, which is a combination the sentence quietly rules out. Marking a field required and then supplying the answer yourself is not a stricter setting; it is two instructions that cancel. Keep default for settings that describe how to work - concurrency, timeouts, depth limits, feature switches. Those are genuinely safe to assume, and an empty call that inherits them is behaving correctly. The distinction is worth stating as a rule: default on a method is a convenience; default on a subject is a substitution. Checking your own Actors I wrote the catalogue sweep as a script so I would stop trusting my memory of my own schemas. It reads every local .actor/ directory, classifies each input and output field into an identifier kind, and reports which of your Actors can be a chain stage and which of them will accept an empty call: node composability.mjs # summary and pair matrix node composability.mjs --rules # print the classifier so you can audit it The --rules flag exists because the classifier is regular expressions over field names, and a regex table you cannot see is a number you should not trust. Mine misfired the first time I ran it: it reported that github-repository-audit requires no identifier, which is nonsense for an Actor that audits repositories. The cause was real and boring - that schema has no required key at all, so every field is optional and the defaults decide everything. If you want a single check with no script, call your own Actor with an empty input and see what happens: apify call <your-actor> --input '{}' If it succeeds, look at what it audited. That is what an agent gets when it forgets. What I changed and what I did not I have not stripped the defaults from all seven Actors yet. The measurement says the harm is concentrated in cold calls, and I want to measure that path on these specific Actors before I change seven live listings - the last time I acted on a confident prediction here, the prediction was wrong twice. What I have changed is the rule I apply to new fields, and the check I run before publishing. Both of them came out of an experiment that failed to prove its own thesis, which is becoming a habit I am not entirely unhappy about.