Skip to main content

Build and publish an Apify Actor

You push a fix, the Console shows a fresh build, and a week later a user reports the bug you already fixed. Nothing errored. The build just never took the tag your users actually run.

How do you build and publish an Apify Actor?​

Install the Apify CLI, scaffold with apify create, write the scraper against Crawlee and the Apify SDK, test with apify run, and deploy with apify push. That puts the Actor in your own account and nowhere else. Publishing to the Store is a separate action in the Console, under the Publishing tab, and that gap is where most first Actors stall.

If the word "Actor" is still fuzzy, start with what an Apify Actor actually is and come back. Everything below assumes you want a listing other people can run, not a script you run yourself.

Five traps sit between that command list and a listing that stays live: a build tag that does not move when a build fails, an input-schema path Apify's own docs disagree on, a daily test with a fixed under-maintenance clock, a concurrent-runs limit two of Apify's own pages dispute, and a publishing commitment with a real legal shape. Each gets its own section below.

What do the four CLI commands actually do?​

npm install -g apify-cli
apify login # credentials go to ~/.apify/auth.json
apify create my-actor
cd my-actor
apify run # local run, output under ./storage
apify push # deploy using .actor/actor.json
CommandWhat Apify's CLI reference says it does
apify create"Creates an Actor project from a template in a new directory. The command automatically initializes a git repository in the newly created Actor directory."
apify run"Executes Actor locally with simulated Apify environment variables. Stores data in local 'storage' directory."
apify push"Deploys Actor to Apify platform using settings from '.actor/actor.json'."
apify login"Authenticates your Apify account and saves credentials to '~/.apify/auth.json'. All other commands use these stored credentials."

Quoted from the Apify CLI command reference, checked 2026-09-09. For Python, apify create my-actor -t python-start scaffolds the same layout around the Python SDK.

A minimal Actor​

import { Actor } from 'apify';
import { CheerioCrawler } from 'crawlee';

await Actor.init();
const input = await Actor.getInput();
const startUrls = input?.startUrls ?? [{ url: 'https://news.ycombinator.com' }];

const crawler = new CheerioCrawler({
async requestHandler({ request, $ }) {
const title = $('.titleline > span > a').first().text().trim();
const href = $('.titleline > span > a').first().attr('href');
await Actor.pushData({
url: request.url,
title,
link: href?.startsWith('http') ? href : new URL(href ?? '', request.url).href,
});
},
});

await crawler.run(startUrls);
await Actor.exit();

Four SDK calls carry the whole contract. Actor.init() "initializes the Actor, enabling support for the Apify platform dynamically based on APIFY_IS_AT_HOME env var", Actor.getInput() "gets the Actor input value from the default KeyValueStore associated with the current Actor run", Actor.pushData() "stores an object or an array of objects to the default Dataset of the current Actor run", and Actor.exit() "gracefully exits the Actor run with the provided status message and exit code" (Apify SDK for JavaScript, Actor class reference, checked 2026-09-09). The SDK JavaScript tutorial goes deeper on each.

A dataset that stays empty after a green run almost always means Actor.pushData() never ran inside the request handler. Check the run log before you touch the selectors.

Why is my pushed Actor still running the old code?​

Because users run a tag, not a build, and the tag only moves when a build succeeds.

Apify numbers every build MAJOR.MINOR.BUILD, for example 1.2.345, where MAJOR.MINOR is the Actor version and BUILD is an auto-incrementing counter starting at 1. Tags sit on top of that. Apify's builds documentation states that when you set a Build tag property, "when a new build of that version is successfully finished, it's automatically assigned the tag", and that "by default, the builds are set to the latest tag" (docs.apify.com, builds, checked 2026-09-09).

Read that conditional carefully. A build that fails does not take the tag. Your Actor page still shows a newer build number, your users still get the previous one, and nothing in the Console shouts about it.

apify push takes both values from your project file rather than inventing them. The -b, --build-tag flag is "build tag to be applied to the successful Actor build. By default, it is taken from the '.actor/actor.json' file", and -v, --version behaves the same way. In .actor/actor.json, buildTag "defaults to latest" if you leave it out.

{
"actorSpecification": 1,
"name": "my-actor",
"version": "0.1",
"buildTag": "latest",
"dockerfile": "./Dockerfile",
"input": "./input_schema.json"
}

Two consequences worth writing on a sticky note:

  1. Bumping version without thinking about buildTag splits your users. Versions are MAJOR.MINOR, and Apify supports running several at once, so you can hold production on 0.1 and develop 0.2 in parallel. That is a feature until you forget which one carries latest.
  2. apify push refuses to overwrite a newer remote version unless you pass --force. That guard fires when someone has edited the source in the Console since your last push. Forcing it throws their edit away.

After any push that matters, open the Actor's Builds tab and confirm which build number currently holds latest. That is the only check that answers the question.

Where does the input schema go: actor.json or INPUT_SCHEMA.json?​

Apify's own two pages disagree on how strongly to discourage the old path, so here is both, dated.

Source (checked 2026-09-09)What it says
actor.json reference"You can embed your input schema object directly in actor.json under the input field. You can also provide a path to a custom input schema. If not provided, the input schema at .actor/INPUT_SCHEMA.json or INPUT_SCHEMA.json is used, in this order of preference." No deprecation flagged.
Input schema specification v1"For backwards compatibility, if the input field is omitted, the system looks for an INPUT_SCHEMA.json file either in the .actor directory or the Actor's top-level directory - but note that this functionality is deprecated and might be removed in the future."

Both are true at once. The fallback still works and still renders the Console form, so nothing breaks today. It is the legacy path, and only one of the two pages tells you so. Use the input field and stop thinking about it:

{
"title": "Hacker News Scraper Input",
"type": "object",
"schemaVersion": 1,
"properties": {
"startUrls": {
"title": "Start URLs",
"type": "array",
"description": "URLs to scrape. Defaults to the Hacker News front page.",
"editor": "requestListSources",
"prefill": [{ "url": "https://news.ycombinator.com" }]
}
}
}

Save that as input_schema.json, point "input": "./input_schema.json" at it, push again. Prefilled values matter more than they look: the Store's automated test runs your Actor on its default input, so a schema with no usable defaults fails that test every day.

How do you publish an Actor to the Apify Store?​

Apify's publishing docs give the path as: "In the left-side panel, go to Development > My Actors. From the table, select the Actor you want to publish. Go to the Publishing tab." Five sections must be completed before the button unlocks:

  1. Display information — "add an Actor logo and description".
  2. Monetization — set up how (or whether) the Actor charges.
  3. Sample output — added automatically if you included it in the input schema.
  4. Output schema — "define the output schema of your Actor", plus optional dataset, key-value store and web server OpenAPI schemas.
  5. Actor permissions — "define the permission level that your Actor requires".

Then "select Publish on Store" (docs.apify.com, publish your Actor, checked 2026-09-09).

Correction, 2026-09-09: this page previously called that tab "Publication". It is Publishing. The section name, Display information, was right.

Pricing and payouts live behind the Monetization section, and they have enough moving parts to deserve their own page: how Apify Actor monetization and payouts work covers pay-per-event, the revenue split and the payout minimums.

What happens when a published Actor breaks?​

This is the part nobody tells you before you publish, and it runs on a fixed clock.

Apify tests every Store Actor daily. "The test runs the Actor with its default input and expects it to finish with a Succeeded status and non-empty default dataset within 5 minutes."

ElapsedWhat happens
3 consecutive failing daysYou get a notification and the listing is labelled under maintenance
+14 more days failingA second notification
31 consecutive days failingThe Actor is deprecated

The two labels do different damage. "The Under maintenance status makes your Actor rank lower in search results." Deprecation is worse: "the deprecated Actors aren't searchable in Apify Store." Sources: automated testing and display Actor status, both checked 2026-09-09.

Recovery is not manual. Fix the Actor and rebuild, and "the automatic testing system will pick this up within 24 hours". If the breakage was the target site's fault rather than yours, the badge also clears on its own: "if your Actor passes the majority of test runs in the next 7 days, it will be marked as healthy automatically." Actors that need authentication, or that legitimately take longer than five minutes, can ask Apify support for an exemption from the test.

Plan monitoring around the three-day window, not the thirty-day one. Advanced Actor monitoring covers alerting on failed runs before the Store test notices.

How many concurrent runs do you actually get?​

Apify's two tier-1 pages disagree, and it matters the moment you schedule anything.

Source (both checked 2026-09-09)Free plan concurrent runs
apify.com/pricing5
docs.apify.com/platform/limits25

They agree on Starter at 32, Scale at 128 and Business at 256. Only the free tier is in dispute. Assume 5 when you design a schedule, and confirm against your own account's limits page before you rely on the higher number. An honest conflict is more useful here than a confident guess.

What does it cost to test an Actor in the cloud?​

The Free plan carries $5 of platform usage a month, which is enough for development runs and not enough for a load test. Starter is $19/month subject to change · verified 2026-09-09, or $17 a month billed annually, and it includes $19 of platform usage at $0.20 per compute unit (apify.com/pricing, checked 2026-09-09). The subscription is a usage credit rather than an access fee, so on a development workload you are mostly buying headroom. The Apify pricing guide works through both billing layers.

Two build limits are worth knowing before you write the Dockerfile, from docs.apify.com/platform/limits (checked 2026-09-09): the build timeout is 1,800 seconds on every plan, and build memory is capped at 4,096 MB. A heavy npm install plus a Playwright browser download can get close to the timeout on a cold build.

When should you not publish to the Store?​

Publishing signs you up to a maintenance commitment with a legal shape. Apify's Store publishing terms are explicit: "By publishing your Actor, you agree to use your best effort to maintain it in working condition and make updates to it from time to time as needed, in order to maintain its continuing functionality" (Store publishing terms, checked 2026-09-09).

Skip the Store if any of these describe you:

  • It is a one-off. A cron job on GitHub Actions running the same Dockerfile costs nothing and owes nobody a fix.
  • You cannot commit to a two-week repair window. Selectors break. Three failing days demotes the listing, thirty-one deprecates it, and neither clock cares why.
  • The data is the product, not the tool. If buyers want rows rather than a scraper they operate, sell the dataset and keep the code private.
  • You need a hard guarantee that the listing stays up. The same terms let Apify "unpublish, delete, restrict, or modify that Actor or content without prior notice" at its sole discretion where it judges the Actor to breach them. Keep the Actor portable: it is a Dockerfile and a main.js, and it runs anywhere Docker does.

Distribution is not automatic either. Apify's own Store page advertises "70,000+ ready-to-run tools called Actors" (apify.com/store.md, checked 2026-09-09), and the public Store API returned 57,142 searchable listings on the same day. The two figures count different things; either way your listing arrives into a crowded index. Store search "evaluates parameters similar to those in the Actor quality score", so a healthy test record and real documentation are ranking inputs, not just hygiene (how Apify Store works, checked 2026-09-09).

If you do want the listing, the fastest honest path is: one narrow Actor, defaults that pass the daily test, a README that names the limits, and a fix committed within days of a break.

What changed on this page, and when​

DateChange
2026-09-09Console publishing tab corrected from "Publication" to Publishing, per Apify's publish docs.
2026-09-09Input schema guidance moved to the input field of .actor/actor.json; the INPUT_SCHEMA.json fallback is documented here as legacy, with both conflicting Apify sources quoted.
2026-09-09Added the build-tag behaviour, the daily Store test clock, and the disputed free-plan concurrency figure. Removed an unsourced "under an hour" build-time estimate.

Ready to push the first one? Create a free Apify account and you get $5 of monthly platform usage, no card. You can run apify push and keep the Actor entirely private: nothing about deploying obliges you to list it, and the maintenance clock above only starts when you publish.

Frequently Asked Questions

No. apify push deploys the Actor to your own Apify account using the settings in .actor/actor.json. Store publication is a separate step in the Console under the Publishing tab, where you complete Display information, Monetization, Sample output, Output schema and Actor permissions before selecting Publish on Store.

Users run a build tag, and a tag only moves to a new build when that build finishes successfully. Builds default to the latest tag, and apify push takes the tag from .actor/actor.json. If the build failed, the tag stayed put. Check the Builds tab to see which build number currently holds latest.

Use the input field of .actor/actor.json. Apify's actor.json reference documents .actor/INPUT_SCHEMA.json as a working fallback with no deprecation note, while the input schema v1 specification calls that same fallback deprecated and says it might be removed. Both pages were checked on 2026-09-09.

Apify runs a daily automated test with your default input, expecting a Succeeded status and a non-empty default dataset within five minutes. Three consecutive failing days label the Actor under maintenance, which lowers its Store search ranking. Thirty consecutive failing days deprecate it, and deprecated Actors are not searchable in the Store.

Fix the Actor and rebuild it. Apify's automatic testing picks up the new build within 24 hours. If the failures came from the target site rather than your code, the badge also clears on its own once the Actor passes the majority of test runs over the following seven days.

JavaScript or TypeScript with Crawlee and the Apify SDK for JavaScript, or Python with the Apify SDK for Python via apify create my-actor -t python-start. An Actor is ultimately a Docker image, so any runtime you can containerize will run, but the SDK conveniences are strongest on those two paths.

Yassine El Haddad

Software Developer & Automation Specialist

Common mistakes and fixes

I pushed a fix, but users still get the old behaviour.

Check the build tag, not the build list. A tag moves to a new build only when that build finishes successfully, and users run the tag (latest by default). Open the Actor's Builds tab and confirm which build currently carries 'latest'.

The Console shows raw JSON instead of an input form.

Apify reads the schema from the 'input' field of .actor/actor.json. If you left that field out, it falls back to .actor/INPUT_SCHEMA.json, which Apify's own spec page marks as deprecated. Set the 'input' field and push again.

'apify push' refuses to upload, complaining about a newer remote version.

Someone edited the source in the Console after your last push. Pull those edits down or accept losing them, then re-run with --force. Do not make Console edits a habit if you also push from a repo.

My Store listing shows an 'under maintenance' badge I never set.

Apify's daily automated test failed for three consecutive days. Fix the Actor and rebuild; the test system re-checks within 24 hours, or the listing clears automatically after passing most runs over 7 days.