From 18472158f3ca1603d3b3a585586429343c2ce843 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Tue, 6 Oct 2026 15:19:28 +0200 Subject: [PATCH 1/2] docs: document named child runs --- .../06_interacting_with_other_actors.mdx | 89 +++++++++++++++++++ .../code/06_interacting_abort_with_parent.py | 20 +++++ .../code/06_interacting_child_run_budget.py | 35 ++++++++ .../code/06_interacting_child_run_limit.py | 30 +++++++ .../code/06_interacting_child_runs.py | 31 +++++++ .../code/06_interacting_named_call.py | 21 +++++ 6 files changed, 226 insertions(+) create mode 100644 docs/02_concepts/code/06_interacting_abort_with_parent.py create mode 100644 docs/02_concepts/code/06_interacting_child_run_budget.py create mode 100644 docs/02_concepts/code/06_interacting_child_run_limit.py create mode 100644 docs/02_concepts/code/06_interacting_child_runs.py create mode 100644 docs/02_concepts/code/06_interacting_named_call.py diff --git a/docs/02_concepts/06_interacting_with_other_actors.mdx b/docs/02_concepts/06_interacting_with_other_actors.mdx index 1a362d7a1..21796898b 100644 --- a/docs/02_concepts/06_interacting_with_other_actors.mdx +++ b/docs/02_concepts/06_interacting_with_other_actors.mdx @@ -8,6 +8,11 @@ import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock'; import InteractingStartExample from '!!raw-loader!roa-loader!./code/06_interacting_start.py'; import InteractingCallExample from '!!raw-loader!roa-loader!./code/06_interacting_call.py'; +import InteractingNamedCallExample from '!!raw-loader!roa-loader!./code/06_interacting_named_call.py'; +import InteractingChildRunsExample from '!!raw-loader!roa-loader!./code/06_interacting_child_runs.py'; +import InteractingAbortWithParentExample from '!!raw-loader!roa-loader!./code/06_interacting_abort_with_parent.py'; +import InteractingChildRunLimitExample from '!!raw-loader!roa-loader!./code/06_interacting_child_run_limit.py'; +import InteractingChildRunBudgetExample from '!!raw-loader!roa-loader!./code/06_interacting_child_run_budget.py'; import InteractingCallTaskExample from '!!raw-loader!roa-loader!./code/06_interacting_call_task.py'; import InteractingMetamorphExample from '!!raw-loader!roa-loader!./code/06_interacting_metamorph.py'; import InteractingAbortExample from '!!raw-loader!roa-loader!./code/06_interacting_abort.py'; @@ -34,6 +39,90 @@ The `Actor.call` method starts another {InteractingCallExample} +## Named child runs + +When your Actor migrates to another server or is resurrected, it starts again from the beginning. An unnamed `Actor.start` or `Actor.call` then starts a second child run, and the first one keeps running with nobody waiting for it. + +To avoid the duplicate, pass a `name` to `Actor.start` or `Actor.call`. The name must be unique within your Actor run. Right after the child run starts, the SDK records its name and run ID under the `APIFY_CHILD_RUNS` key in the default key-value store. After a restart, the same call looks up the recorded run and reuses it based on its status: + +- `READY` or `RUNNING`: the call reattaches to the run. +- `SUCCEEDED`: the call returns the run as is. +- `ABORTED` or `TIMED-OUT`: the call resurrects the run. A run that is still `ABORTING` or `TIMING-OUT` is waited for first. +- `FAILED`, or the run no longer exists: the call starts a new run under the same name. + +A named `Actor.call` that reattaches to a run streams only the log lines the child writes from then on, so the parent log doesn't repeat what the previous attempt already printed. + + + {InteractingNamedCallExample} + + +Note that: + +- The name is bound to the `actor_id` you pass. Using the same name for a different Actor, or for the same Actor referenced by its ID instead of its name, raises a `ValueError`. +- Concurrent calls under one name in the same Actor run share a single child run. +- If your Actor is killed after the platform starts the child but before the SDK records it, the child run isn't recorded and the next attempt starts a new one. + +### Listing child runs + +The `Actor.child_runs` method returns the named child runs of your Actor run by name, including those started before a migration or resurrection. Each entry has the run's current state as the API returns it, and the IDs of earlier runs under the same name that failed or went missing and were replaced. Runs started without a `name` aren't listed. + + + {InteractingChildRunsExample} + + +### Aborting child runs with the parent + +When your Actor run is aborted, its child runs keep running, and you pay for them until they finish on their own. To abort a named child run together with your Actor run, pass `abort_with_parent=True`. When your Actor run receives the `ABORTING` event of a graceful abort, the SDK gracefully aborts every child run marked this way that's still `READY` or `RUNNING`. The flag is recorded with the name, so it also covers child runs started before a migration or resurrection. + + + {InteractingAbortWithParentExample} + + +Note that: + +- The option is off by default, since aborting a child run throws away the work it hasn't finished. +- It requires `name`. Without one, `Actor.start` and `Actor.call` raise a `ValueError`. +- Each call under a name records its own value, so the latest call decides whether the run is aborted. +- A child run aborted this way ends as `ABORTED`. If your Actor run is resurrected later, the same named call resurrects the child run too. +- Only a graceful abort gives the SDK time to act. A hard abort, a timeout, or a crash of your Actor run leaves the child runs running. +- Child runs started after your Actor run received `ABORTING` aren't aborted, so don't start new ones while it's shutting down. +- A child run started with its own `token` is aborted with that token. After a migration or resurrection, the SDK uses your Actor's token for it until the same named call runs again. If that token can't access the child run, the abort fails and the error is logged. + +### Limiting concurrent child runs + +An Actor that starts many child runs at once can hit the concurrency or memory limit of your account. To cap how many named child runs are active at once, call `Actor.set_child_run_limits`. While the limit is reached, a named `Actor.start` or `Actor.call` that would start or resurrect a run waits until one of the active child runs finishes. The SDK counts the child runs from the registry, so child runs started before a migration or resurrection count too. + + + {InteractingChildRunLimitExample} + + +Note that: + +- A child run counts as active while it's `READY`, `RUNNING`, `ABORTING` or `TIMING-OUT`. +- Only named child runs count, and only named calls wait. A call without `name` starts its run right away. +- Reattaching to an active child run never waits, since the run already holds a slot. +- `Actor.call` frees the slot as soon as its run finishes. The SDK doesn't learn right away about a child run that nothing waits for, so it fetches the run again before counting it, once its status is more than 10 seconds old. +- The limit isn't persisted. After a migration or resurrection, call `Actor.set_child_run_limits` again before you start child runs. +- Once your Actor run receives the `ABORTING` event, a call waiting for a slot raises a `RuntimeError` without starting its run. + +### Sharing the charge budget with child runs + +When your Actor run is started with a maximum total charge (`max_total_charge_usd`), its named child runs share that budget. Each named `Actor.start` or `Actor.call` reserves a charge limit for its child run from the part of the budget your Actor run hasn't charged yet and hasn't reserved for other child runs. Without `max_total_charge_usd`, the child run gets all of that part. A higher value is lowered to it. Your Actor run can't charge the reserved part itself, so the whole tree of runs stays within the budget, however many child runs start at once. + + + {InteractingChildRunBudgetExample} + + +Note that: + +- Pass `max_total_charge_usd` when several child runs run at once. Otherwise the first one reserves the whole budget, and the next named start raises a `RuntimeError`. +- When a child run finishes, the SDK keeps only its charge (`usage_total_usd`) reserved and releases the rest. The platform can add to that charge for about 3 minutes after the run finishes, so the SDK fetches the run again until then. +- The charges of a failed child run stay reserved after a new run replaces it under the same name. +- A reattached child run keeps the limit it was started with. A resurrected one gets a new limit, which can include the part it reserved before. +- The reservations are stored in the registry, so they survive a migration or resurrection of your Actor run. +- Child runs started without `name` aren't tracked, so they don't reserve any part of the budget. +- A limit that the platform sets by default, which it does for pay-per-event Actors, isn't shared. Only a limit set for the run counts. + ## Actor call task The `Actor.call_task` method starts an [Actor task](https://docs.apify.com/platform/actors/tasks) on the Apify platform, and waits for the started Actor run to finish. diff --git a/docs/02_concepts/code/06_interacting_abort_with_parent.py b/docs/02_concepts/code/06_interacting_abort_with_parent.py new file mode 100644 index 000000000..44d39c9e5 --- /dev/null +++ b/docs/02_concepts/code/06_interacting_abort_with_parent.py @@ -0,0 +1,20 @@ +import asyncio + +from apify import Actor + + +async def main() -> None: + async with Actor: + # Start the child run, and abort it if this Actor run is gracefully aborted. + actor_run = await Actor.start( + actor_id='apify/screenshot-url', + run_input={'urls': [{'url': 'https://www.apify.com/'}]}, + name='screenshot', + abort_with_parent=True, + ) + + Actor.log.info(f'Started child run {actor_run.id}') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/docs/02_concepts/code/06_interacting_child_run_budget.py b/docs/02_concepts/code/06_interacting_child_run_budget.py new file mode 100644 index 000000000..c7e9e590d --- /dev/null +++ b/docs/02_concepts/code/06_interacting_child_run_budget.py @@ -0,0 +1,35 @@ +import asyncio +from decimal import Decimal + +from apify import Actor + + +async def main() -> None: + async with Actor: + # The budget this Actor run was started with, shared with its named child runs. + budget = Actor.get_charging_manager().get_pricing_info().max_total_charge_usd + Actor.log.info(f'Budget of this run: {budget} USD') + + # Give each of the three child runs a quarter of the budget, so they can all + # start at once and this run keeps the rest for its own charges. + per_child = Decimal(1) if budget.is_infinite() else budget / 4 + + actor_runs = await asyncio.gather( + *( + Actor.call( + actor_id='apify/screenshot-url', + run_input={'urls': [{'url': f'https://www.apify.com/?page={page}'}]}, + name=f'screenshot-{page}', + max_total_charge_usd=per_child, + ) + for page in range(3) + ) + ) + + for actor_run in actor_runs: + cost = actor_run.usage_total_usd + Actor.log.info(f'Child run {actor_run.id} cost {cost} USD') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/docs/02_concepts/code/06_interacting_child_run_limit.py b/docs/02_concepts/code/06_interacting_child_run_limit.py new file mode 100644 index 000000000..3da343b63 --- /dev/null +++ b/docs/02_concepts/code/06_interacting_child_run_limit.py @@ -0,0 +1,30 @@ +import asyncio + +from apify import Actor + + +async def main() -> None: + async with Actor: + # Keep at most 3 named child runs active at once. + Actor.set_child_run_limits(max_concurrent_runs=3) + + urls = [f'https://www.apify.com/?page={page}' for page in range(6)] + + # Each call waits for a free slot before it starts its child run. + actor_runs = await asyncio.gather( + *( + Actor.call( + actor_id='apify/screenshot-url', + run_input={'urls': [{'url': url}]}, + name=f'screenshot-{index}', + ) + for index, url in enumerate(urls) + ) + ) + + for actor_run in actor_runs: + Actor.log.info(f'Child run {actor_run.id} finished as {actor_run.status}') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/docs/02_concepts/code/06_interacting_child_runs.py b/docs/02_concepts/code/06_interacting_child_runs.py new file mode 100644 index 000000000..56cee8e15 --- /dev/null +++ b/docs/02_concepts/code/06_interacting_child_runs.py @@ -0,0 +1,31 @@ +import asyncio +from collections import Counter + +from apify import Actor + + +async def main() -> None: + async with Actor: + for region in ['eu', 'us']: + await Actor.start( + actor_id='apify/screenshot-url', + run_input={'urls': [{'url': f'https://www.apify.com/?region={region}'}]}, + name=f'screenshot-{region}', + ) + + # Count the child runs by their current status. + child_runs = await Actor.child_runs() + statuses = Counter( + child_run.run.status if child_run.run else 'MISSING' + for child_run in child_runs.values() + ) + Actor.log.info(f'Child runs by status: {dict(statuses)}') + + # Report the names whose earlier runs were replaced. + for name, child_run in child_runs.items(): + if replaced := len(child_run.previous_run_ids): + Actor.log.info(f'{name} was replaced {replaced} time(s)') + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/docs/02_concepts/code/06_interacting_named_call.py b/docs/02_concepts/code/06_interacting_named_call.py new file mode 100644 index 000000000..ac7f58f6f --- /dev/null +++ b/docs/02_concepts/code/06_interacting_named_call.py @@ -0,0 +1,21 @@ +import asyncio + +from apify import Actor + + +async def main() -> None: + async with Actor: + # Call the apify/screenshot-url Actor under the name 'screenshot'. If this run + # migrates while the child is running, the same call after the restart waits + # for the recorded child run instead of starting a new one. + actor_run = await Actor.call( + actor_id='apify/screenshot-url', + run_input={'urls': [{'url': 'https://www.apify.com/'}]}, + name='screenshot', + ) + + Actor.log.info(f'Child run {actor_run.id} finished with {actor_run.status}') + + +if __name__ == '__main__': + asyncio.run(main()) From 7adac64db171650db9da90c19f50a7906a19b18e Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Wed, 7 Oct 2026 08:59:33 +0200 Subject: [PATCH 2/2] docs: rename the child run `name` parameter to `run_name` and cover `Actor.call_task` --- .../06_interacting_with_other_actors.mdx | 17 +++++++++-------- .../code/06_interacting_abort_with_parent.py | 2 +- .../code/06_interacting_child_run_budget.py | 2 +- .../code/06_interacting_child_run_limit.py | 2 +- .../code/06_interacting_child_runs.py | 2 +- .../code/06_interacting_named_call.py | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/02_concepts/06_interacting_with_other_actors.mdx b/docs/02_concepts/06_interacting_with_other_actors.mdx index 21796898b..e6bf6fdb2 100644 --- a/docs/02_concepts/06_interacting_with_other_actors.mdx +++ b/docs/02_concepts/06_interacting_with_other_actors.mdx @@ -43,7 +43,7 @@ The `Actor.call` method starts another When your Actor migrates to another server or is resurrected, it starts again from the beginning. An unnamed `Actor.start` or `Actor.call` then starts a second child run, and the first one keeps running with nobody waiting for it. -To avoid the duplicate, pass a `name` to `Actor.start` or `Actor.call`. The name must be unique within your Actor run. Right after the child run starts, the SDK records its name and run ID under the `APIFY_CHILD_RUNS` key in the default key-value store. After a restart, the same call looks up the recorded run and reuses it based on its status: +To avoid the duplicate, pass a `run_name` to `Actor.start`, `Actor.call` or `Actor.call_task`. The name must be unique within your Actor run. Right after the child run starts, the SDK records its name and run ID under the `APIFY_CHILD_RUNS` key in the default key-value store. After a restart, the same call looks up the recorded run and reuses it based on its status: - `READY` or `RUNNING`: the call reattaches to the run. - `SUCCEEDED`: the call returns the run as is. @@ -58,13 +58,13 @@ A named `Actor.call` that reattaches to a run streams only the log lines the chi Note that: -- The name is bound to the `actor_id` you pass. Using the same name for a different Actor, or for the same Actor referenced by its ID instead of its name, raises a `ValueError`. +- The name is bound to the `actor_id` or `task_id` you pass. Using the same name for a different Actor or task, or for the same Actor referenced by its ID instead of its name, raises a `ValueError`. - Concurrent calls under one name in the same Actor run share a single child run. - If your Actor is killed after the platform starts the child but before the SDK records it, the child run isn't recorded and the next attempt starts a new one. ### Listing child runs -The `Actor.child_runs` method returns the named child runs of your Actor run by name, including those started before a migration or resurrection. Each entry has the run's current state as the API returns it, and the IDs of earlier runs under the same name that failed or went missing and were replaced. Runs started without a `name` aren't listed. +The `Actor.child_runs` method returns the named child runs of your Actor run by name, including those started before a migration or resurrection. Each entry has the run's current state as the API returns it, and the IDs of earlier runs under the same name that failed or went missing and were replaced. Runs started without a `run_name` aren't listed. {InteractingChildRunsExample} @@ -81,7 +81,7 @@ When your Actor run is aborted, its child runs keep running, and you pay for the Note that: - The option is off by default, since aborting a child run throws away the work it hasn't finished. -- It requires `name`. Without one, `Actor.start` and `Actor.call` raise a `ValueError`. +- It requires `run_name`. Without one, `Actor.start`, `Actor.call` and `Actor.call_task` raise a `ValueError`. - Each call under a name records its own value, so the latest call decides whether the run is aborted. - A child run aborted this way ends as `ABORTED`. If your Actor run is resurrected later, the same named call resurrects the child run too. - Only a graceful abort gives the SDK time to act. A hard abort, a timeout, or a crash of your Actor run leaves the child runs running. @@ -90,7 +90,7 @@ Note that: ### Limiting concurrent child runs -An Actor that starts many child runs at once can hit the concurrency or memory limit of your account. To cap how many named child runs are active at once, call `Actor.set_child_run_limits`. While the limit is reached, a named `Actor.start` or `Actor.call` that would start or resurrect a run waits until one of the active child runs finishes. The SDK counts the child runs from the registry, so child runs started before a migration or resurrection count too. +An Actor that starts many child runs at once can hit the concurrency or memory limit of your account. To cap how many named child runs are active at once, call `Actor.set_child_run_limits`. While the limit is reached, a named `Actor.start`, `Actor.call` or `Actor.call_task` that would start or resurrect a run waits until one of the active child runs finishes. The SDK counts the child runs from the registry, so child runs started before a migration or resurrection count too. {InteractingChildRunLimitExample} @@ -99,9 +99,9 @@ An Actor that starts many child runs at once can hit the concurrency or memory l Note that: - A child run counts as active while it's `READY`, `RUNNING`, `ABORTING` or `TIMING-OUT`. -- Only named child runs count, and only named calls wait. A call without `name` starts its run right away. +- Only named child runs count, and only named calls wait. A call without `run_name` starts its run right away. - Reattaching to an active child run never waits, since the run already holds a slot. -- `Actor.call` frees the slot as soon as its run finishes. The SDK doesn't learn right away about a child run that nothing waits for, so it fetches the run again before counting it, once its status is more than 10 seconds old. +- `Actor.call` and `Actor.call_task` free the slot as soon as their run finishes. The SDK doesn't learn right away about a child run that nothing waits for, so it fetches the run again before counting it, once its status is more than 10 seconds old. - The limit isn't persisted. After a migration or resurrection, call `Actor.set_child_run_limits` again before you start child runs. - Once your Actor run receives the `ABORTING` event, a call waiting for a slot raises a `RuntimeError` without starting its run. @@ -120,7 +120,8 @@ Note that: - The charges of a failed child run stay reserved after a new run replaces it under the same name. - A reattached child run keeps the limit it was started with. A resurrected one gets a new limit, which can include the part it reserved before. - The reservations are stored in the registry, so they survive a migration or resurrection of your Actor run. -- Child runs started without `name` aren't tracked, so they don't reserve any part of the budget. +- Child runs started without `run_name` aren't tracked, so they don't reserve any part of the budget. +- A task run can't be given a charge limit, so a named `Actor.call_task` that would start a new run raises a `RuntimeError`. - A limit that the platform sets by default, which it does for pay-per-event Actors, isn't shared. Only a limit set for the run counts. ## Actor call task diff --git a/docs/02_concepts/code/06_interacting_abort_with_parent.py b/docs/02_concepts/code/06_interacting_abort_with_parent.py index 44d39c9e5..35d603bdd 100644 --- a/docs/02_concepts/code/06_interacting_abort_with_parent.py +++ b/docs/02_concepts/code/06_interacting_abort_with_parent.py @@ -9,7 +9,7 @@ async def main() -> None: actor_run = await Actor.start( actor_id='apify/screenshot-url', run_input={'urls': [{'url': 'https://www.apify.com/'}]}, - name='screenshot', + run_name='screenshot', abort_with_parent=True, ) diff --git a/docs/02_concepts/code/06_interacting_child_run_budget.py b/docs/02_concepts/code/06_interacting_child_run_budget.py index c7e9e590d..b7325e1a8 100644 --- a/docs/02_concepts/code/06_interacting_child_run_budget.py +++ b/docs/02_concepts/code/06_interacting_child_run_budget.py @@ -19,7 +19,7 @@ async def main() -> None: Actor.call( actor_id='apify/screenshot-url', run_input={'urls': [{'url': f'https://www.apify.com/?page={page}'}]}, - name=f'screenshot-{page}', + run_name=f'screenshot-{page}', max_total_charge_usd=per_child, ) for page in range(3) diff --git a/docs/02_concepts/code/06_interacting_child_run_limit.py b/docs/02_concepts/code/06_interacting_child_run_limit.py index 3da343b63..75e340092 100644 --- a/docs/02_concepts/code/06_interacting_child_run_limit.py +++ b/docs/02_concepts/code/06_interacting_child_run_limit.py @@ -16,7 +16,7 @@ async def main() -> None: Actor.call( actor_id='apify/screenshot-url', run_input={'urls': [{'url': url}]}, - name=f'screenshot-{index}', + run_name=f'screenshot-{index}', ) for index, url in enumerate(urls) ) diff --git a/docs/02_concepts/code/06_interacting_child_runs.py b/docs/02_concepts/code/06_interacting_child_runs.py index 56cee8e15..fcf9ef757 100644 --- a/docs/02_concepts/code/06_interacting_child_runs.py +++ b/docs/02_concepts/code/06_interacting_child_runs.py @@ -10,7 +10,7 @@ async def main() -> None: await Actor.start( actor_id='apify/screenshot-url', run_input={'urls': [{'url': f'https://www.apify.com/?region={region}'}]}, - name=f'screenshot-{region}', + run_name=f'screenshot-{region}', ) # Count the child runs by their current status. diff --git a/docs/02_concepts/code/06_interacting_named_call.py b/docs/02_concepts/code/06_interacting_named_call.py index ac7f58f6f..2be5acc07 100644 --- a/docs/02_concepts/code/06_interacting_named_call.py +++ b/docs/02_concepts/code/06_interacting_named_call.py @@ -11,7 +11,7 @@ async def main() -> None: actor_run = await Actor.call( actor_id='apify/screenshot-url', run_input={'urls': [{'url': 'https://www.apify.com/'}]}, - name='screenshot', + run_name='screenshot', ) Actor.log.info(f'Child run {actor_run.id} finished with {actor_run.status}')