Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
55ed3c5
feat: Validate Actor input and apply input-schema defaults
claude Sep 22, 2026
a2cba42
Merge remote-tracking branch 'origin/master' into claude/brave-allen-…
claude Sep 22, 2026
f6b92eb
docs: Tighten input-schema docs and comments to the new conventions
claude Sep 22, 2026
8cf4c5c
Merge remote-tracking branch 'origin/master' into claude/brave-allen-…
claude Sep 23, 2026
ec796ff
test: Assert the schema defaults land in the run's INPUT record
claude Sep 23, 2026
ea79163
refactor: Share the pushed-source lookup and trim the input-schema code
claude Sep 23, 2026
177df3f
docs: Rewrite the input-schema requirements to the contribution conve…
claude Sep 23, 2026
1472ddb
feat: Let the sample Actors take their defaults from the input schema
claude Sep 23, 2026
eba700e
feat: Log the start pre-charge and warn on platform-incompatible memory
claude Sep 23, 2026
509cdf2
feat: Price the per-dataset-item synthetic event in the sample Actors
claude Sep 23, 2026
0929127
fix: Persist a run's sampled telemetry before its status turns terminal
claude Sep 23, 2026
8e4d3f8
docs: Say how to price an Actor that already has a pricing
claude Sep 23, 2026
e5dd874
feat: Name the field that breaks an append-only pricing update
claude Sep 23, 2026
457076c
test: Compare rounded USD figures exactly instead of within a tolerance
claude Sep 23, 2026
a25cf0b
refactor: Drop the rules around the runtime's dev-folder block
claude Sep 23, 2026
630e13c
docs: State the input-schema requirements as the platform's, plus dif…
claude Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ In a build or run log, everything the runtime itself has to say - dev-folder not
attach line, the browser-view URL, migration markers, a run that could not be started - opens with a
blue `[actor-runtime]` prefix. Your Actor's own output is passed through byte for byte.

## Input schema: defaults and validation

An Actor that declares an input schema (the `input` field of `.actor/actor.json`, `.actor/INPUT_SCHEMA.json`,
or `INPUT_SCHEMA.json` at its root) gets the platform's behaviour locally: the schema's defaults are filled into
every run's input, and an input the schema rejects fails the call with the API's own message instead of starting a
container.

```bash
apify call # runs on the schema's defaults
apify call --input '{"maxPages":0}' # 400 Input is not valid: Field input.maxPages must be >= 1
```

The schema is read at build time, so editing it locally needs an `apify push` even under a registered dev folder,
and a schema the Apify meta-schema rejects fails the build with the defect in its log. Proxy group availability is
not checked locally, and encrypted secret input fields stay unsupported - see
`requirements/actor-driver.md`'s "Input schema, validation and defaults".

## Running with Podman instead of Docker

The runtime talks to the container engine only through its Docker-compatible API socket, and Podman
Expand Down Expand Up @@ -218,7 +235,9 @@ do. Like Python debug mode, this needs the runtime to run from its own built ima

Both bundled samples charge two events when their Actor is priced - `page-scraped` once per page and
`crawl-finished` once at the end - and ship the pricing that defines them in `pricing.json`, so a run
charges for real right after a push:
charges for real right after a push. That pricing also declares both synthetic events, which the Actor
never charges itself: `apify-actor-start` at run start (once per whole GB of the run's memory) and
`apify-default-dataset-item` per item pushed to the default dataset.

```bash
cd sample_actor_ts # or sample_actor_py
Expand All @@ -236,6 +255,19 @@ run's console page and the runs list show the same figures.
platform: an update sends the Actor's existing entries unchanged plus at most one new one, starting after
all of them. Making the Actor free again is therefore appending a `{"pricingModel": "FREE"}` entry.

The `PUT` above therefore prices an Actor that has no pricing yet. Once it has one, a second `PUT` of the
same file is refused (`pricingInfos[0] differs from the Actor's existing pricing info`) - the stored
entries carry the timestamps they were given, which the file does not. Append the file's entry to what
the Actor already has instead:

```bash
apify api PUT /v2/actors/<actorId> --body "$(apify api GET /v2/actors/<actorId> |
jq --argjson new "$(jq '.pricingInfos[-1]' pricing.json)" '{pricingInfos: (.data.pricingInfos + [$new])}')"
```

The Actor's console page has the same thing as a form: the box holds the stored array, and adding an
entry below the existing ones does it without the shell.

Cap a run's spend the way a user does - the cap is a query parameter, with no `apify call` flag for it:

```bash
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
"test:watch": "vitest"
},
"dependencies": {
"@apify/input_schema": "^3.29.2",
"@crawlee/core": "4.0.0-beta.145",
"@crawlee/fs-storage": "4.0.0-beta.145",
"@novnc/novnc": "1.7.0",
"ajv": "^8.20.0",
"dockerode": "^4.0.5",
"express": "^5.1.0",
"json5": "^2.2.3",
Expand Down
97 changes: 97 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions requirements/actor-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@
4. the platform's bundled default Dockerfile, for that build only - the pushed source itself is unchanged.
- Matching is case-insensitive, exact-case wins ties, and every outcome is stated in the build log.

# Input schema, validation and defaults

- **Input schemas work as on the Apify platform**: the schema is read from the pushed source when the
Actor is built - the `input` field of `.actor/actor.json`, else `.actor/INPUT_SCHEMA.json`, else
`INPUT_SCHEMA.json` - a build whose schema cannot be read or is not a valid input schema fails with
the reason in its log, and every run of a build is validated against that build's schema with its
defaults applied, a rejected input starting nothing (`api.md`). A build with no input schema takes
every input exactly as the caller sent it.
- **Differences**: Apify Proxy group availability is not checked, so any `apifyProxyGroups` selection
is accepted, while the rest of a `proxy` field is still validated; encrypted secret input fields
stay unsupported (`unsupported.md`).
- An Actor running from a registered dev folder uses its last build's schema: unlike a source edit,
an edited input schema takes effect only after `apify push`.

# Bind mount volumes with Actor source code

- To let an Actor be re-run with source changes and no rebuild, the Actor's registered local dev
Expand Down Expand Up @@ -215,6 +229,8 @@ start`, ...) is refused by name, naming both the `CMD` fix and how to clear debu
per subscription tier is charged at the lowest paid tier; nothing is ever billed or paid out; and the
rules tying a price change to payout details, notice periods and subscription tiers do not apply.
- An Actor's own charging code therefore runs here unchanged, with no local-testing switch.
- A run's log states what it was pre-charged for starting, so the count is visible where the platform
leaves it to be discovered on the bill.
- The pricing can also be set from the console (`console.md`).

# Users
Expand Down
12 changes: 9 additions & 3 deletions requirements/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
- `DELETE /v2/actor-builds/:buildId` and `DELETE /v2/actor-runs/:runId` on a **non-terminal** build/run
are rejected, not aborted-then-deleted: `400` with error type `deleting-unfinished-build` (builds) or
`cannot-remove-running-run` (runs), matching the Apify platform.
- `POST /v2/actors/:actorId/runs` validates the input against the input schema of the build it
resolved, when that build has one (`actor-driver.md`), and starts nothing when it does not pass:
`400` `invalid-input` for a body that is not `application/json`, is not parseable JSON, is not a
JSON object, or that the schema rejects, naming every offending field; `400` `invalid-input-schema`
when the Actor's own schema is not valid. Both messages match the Apify platform's. A build with no
input schema accepts any body, unvalidated.
- Four endpoints are exceptions to the `{data}` envelope:
- `GET /v2/logs/:buildOrRunId` (and its `actor-builds`/`actor-runs` aliases): the body is plain text,
never `{data}`-wrapped, matching apify-client-js's `log().get()`.
Expand Down Expand Up @@ -305,9 +311,9 @@ This runtime emulates that observable experience on demand:
- `fallbackNotFoundEnabled` covers a request that reaches a route this runtime does serve, but
whose specific record id doesn't exist locally (`record-not-found`, see "Response envelopes"
above).
- Every other error type - `invalid-request`, `user-not-authenticated`,
`cannot-remove-running-run`, `deleting-unfinished-build`, any `dev-folder-*` type,
`internal-error` - is never relayed, regardless of either toggle's state.
- Every other error type - `invalid-request`, `invalid-input`, `invalid-input-schema`,
`user-not-authenticated`, `cannot-remove-running-run`, `deleting-unfinished-build`, any
`dev-folder-*` type, `internal-error` - is never relayed, regardless of either toggle's state.
- **All HTTP methods are eligible for both toggles, writes included**: a `POST`/`PUT`/`DELETE` that
would otherwise 404/501 locally is relayed exactly like a `GET` when its toggle is on - and, if the
platform accepts it, becomes a real write against the caller's real account. This is a deliberate
Expand Down
1 change: 1 addition & 0 deletions requirements/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Test case must verify full Actor development flow:
- Push and build Actor in local actor runtime `apify push`
- Run each sample Actor in the local actor runtime with `apify call --input '{"maxPages":N}'` for at least two different values of `N`, waiting for each run to finish
- Assert via `apify datasets info <default dataset id>` that the default dataset's `itemCount` tracks `N` - the assertion is input-dependent, not just "some items exist"
- Cover the input schema through the CLI too (`actor-driver.md`): `apify call` with no `--input` must run on the schema's defaults, asserted from the run's own `INPUT`, and `apify call` with an input the schema rejects must fail, naming the offending field

## Browser view

Expand Down
4 changes: 2 additions & 2 deletions requirements/unsupported.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ real account, not in the runtime.
- Ad-hoc webhooks on run start
- Metered usage other than compute units: storage operations, data transfer and proxy
- Billing: a run's charges and costs are reported, never invoiced or paid out
- Input validation and defaults from the input schema
- Encrypted secret input fields
- Actor-level default run options
- Dynamic and bounded memory from `.actor/actor.json`
Expand Down Expand Up @@ -101,7 +100,8 @@ real account, not in the runtime.

## Platform limits not enforced

- Memory steps and bounds (128 MB - 32 GB, powers of two)
- Memory steps and bounds (128 MB - 32 GB, powers of two); a run asking for anything else is warned
about in its log and started with it anyway
- Record, item and input size limits
- Concurrent run, rate and per-account quotas
- Process, file-descriptor and shared-memory limits
Expand Down
6 changes: 4 additions & 2 deletions sample_actor_crawler/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ async def request_handler(context: ParselCrawlingContext) -> None:

async def main() -> None:
async with Actor:
# Both fields have a `default` in the input schema, so the runtime fills them in before the
# run starts (the Apify platform does the same) - the Actor needs no fallback of its own.
actor_input = await Actor.get_input() or {}
start_url = actor_input.get("startUrl", "https://crawlee.dev")
start_url = actor_input["startUrl"]

proxy_configuration = await Actor.create_proxy_configuration(
actor_proxy_input=actor_input.get("proxyConfiguration")
actor_proxy_input=actor_input["proxyConfiguration"]
)

crawler = ParselCrawler(
Expand Down
Loading
Loading