Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 74 additions & 28 deletions docs/02_concepts/01_actor_lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,98 @@ title: Actor lifecycle
---

import CodeBlock from '@theme/CodeBlock';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

import ClassContextExample from '!!raw-loader!./code/01_class_context.py';
import ClassManualExample from '!!raw-loader!./code/01_class_manual.py';
import InstanceContextExample from '!!raw-loader!./code/01_instance_context.py';
import InstanceManualExample from '!!raw-loader!./code/01_instance_manual.py';

import ErrorHandlingContextExample from '!!raw-loader!./code/01_error_handling_context.py';
import ErrorHandlingManualExample from '!!raw-loader!./code/01_error_handling_manual.py';

import InitExitExample from '!!raw-loader!./code/01_init_exit.py';
import ContextManagerExample from '!!raw-loader!./code/01_context_manager.py';
import RebootExample from '!!raw-loader!./code/01_reboot.py';

import StatusMessageExample from '!!raw-loader!./code/01_status_message.py';

In this guide, we will show you how to manage the lifecycle of an Apify Actor.
This guide explains how an **Apify Actor** starts, runs, and shuts down, describing the complete Actor lifecycle. For information about the core concepts such as Actors, the Apify Console, storages, and events, check out the [Apify platform documentation](https://docs.apify.com/platform).

## Actor initialization

During initialization, the SDK prepares all the components required to integrate with the Apify platform. It loads configuration from environment variables, initializes access to platform storages such as the [key-value store, dataset, and request queue](https://docs.apify.com/platform/storage), sets up event handling for [platform events](https://docs.apify.com/platform/integrations/webhooks/events), and configures logging.

The recommended approach in Python is to use the global [`Actor`](https://docs.apify.com/sdk/python/reference/class/Actor) class as an asynchronous context manager. This approach automatically manages setup and teardown and keeps your code concise. When entering the context, the SDK loads configuration and initializes clients lazily—for example, a dataset is opened only when it is first accessed. If the Actor runs on the Apify platform, it also begins listening for platform events.

When the Actor exits, either normally or due to an exception, the SDK performs a graceful shutdown. It persists the final Actor state, stops event handling, and sets the terminal exit code together with the [status message](https://docs.apify.com/platform/actors/development/programming-interface/status-messages).

<Tabs groupId="request_queue">
<TabItem value="actor_class_with_context_manager" label="Actor class with context manager" default>
<CodeBlock className="language-python">
{ClassContextExample}
</CodeBlock>
</TabItem>
<TabItem value="actor_class_with_manual_init_exit" label="Actor class with manual init/exit">
<CodeBlock className="language-python">
{ClassManualExample}
</CodeBlock>
</TabItem>
</Tabs>

You can also create an [`Actor`](https://docs.apify.com/sdk/python/reference/class/Actor) instance directly. This does not change its capabilities but allows you to specify optional parameters during initialization, such as disabling automatic `sys.exit()` calls or customizing timeouts. The choice between using a context manager or manual initialization depends on how much control you require over the Actor's startup and shutdown sequence.

<Tabs groupId="request_queue">
<TabItem value="actor_instance_with_context_manager" label="Actor instance with context manager" default>
<CodeBlock className="language-python">
{InstanceContextExample}
</CodeBlock>
</TabItem>
<TabItem value="actor_instance_with_manual_init_exit" label="Actor instance with manual init/exit">
<CodeBlock className="language-python">
{InstanceManualExample}
</CodeBlock>
</TabItem>
</Tabs>

## Error handling

Good error handling lets your Actor fail fast on critical errors, retry transient issues safely, and keep data consistent. Normally you rely on the `async with Actor:` block—if it finishes, the run succeeds (exit code 0); if an unhandled exception occurs, the run fails (exit code 1).

The SDK provides helper methods for explicit control:

- [`Actor.exit`](https://docs.apify.com/sdk/python/reference/class/Actor#exit) - terminates the run successfully (default exit code 0).
- [`Actor.fail`](https://docs.apify.com/sdk/python/reference/class/Actor#fail) - marks the run as failed (default exit code 1).

## Initialization and cleanup
Any non-zero exit code is treated as a `FAILED` run. You rarely need to call these methods directly unless you want to perform a controlled shutdown or customize the exit behavior.

At the start of its runtime, the Actor needs to initialize itself, its event manager and its storages, and at the end of the runtime it needs to close these cleanly. The Apify SDK provides several options on how to manage this.
Catch exceptions only when necessary - for example, to retry network timeouts or map specific errors to exit codes. Keep retry loops bounded with backoff and re-raise once exhausted. Make your processing idempotent so that restarts don't corrupt results. Both [`Actor.exit`](https://docs.apify.com/sdk/python/reference/class/Actor#exit) and [`Actor.fail`](https://docs.apify.com/sdk/python/reference/class/Actor#fail) perform the same cleanup, so complete any long-running persistence before calling them.

The [`Actor.init`](../../reference/class/Actor#init) method initializes the Actor, the event manager which processes the Actor events from the platform event websocket, and the storage client used in the execution environment. It should be called before performing any other Actor operations.
Below is a minimal context-manager example where an unhandled exception automatically fails the run, followed by a manual pattern giving you more control.

The [`Actor.exit`](../../reference/class/Actor#exit) method then exits the Actor cleanly, tearing down the event manager and the storage client. There is also the [`Actor.fail`](../../reference/class/Actor#fail) method, which exits the Actor while marking it as failed.
<CodeBlock className="language-python">{ErrorHandlingContextExample}</CodeBlock>

<CodeBlock className="language-python">
{InitExitExample}
</CodeBlock>
If you need explicit control over exit codes or status messages, you can manage the Actor manually using [`Actor.init`](https://docs.apify.com/sdk/python/reference/class/Actor#init), [`Actor.exit`](https://docs.apify.com/sdk/python/reference/class/Actor#exit), and [`Actor.fail`](https://docs.apify.com/sdk/python/reference/class/Actor#fail).

### Context manager
<CodeBlock className="language-python">{ErrorHandlingManualExample}</CodeBlock>

So that you don't have to call the lifecycle methods manually, the [`Actor`](../../reference/class/Actor) class provides a context manager, which calls the [`Actor.init`](../../reference/class/Actor#init) method on enter, the [`Actor.exit`](../../reference/class/Actor#exit) method on a clean exit, and the [`Actor.fail`](../../reference/class/Actor#fail) method when there is an exception during the run of the Actor.
## Reboot

This is the recommended way to work with the `Actor` class.
Rebooting (available on the Apify platform only) instructs the platform worker to restart your Actor from the beginning of its execution. Use this mechanism only for transient conditions that are likely to resolve after a fresh start — for example, rotating a blocked proxy pool or recovering from a stuck browser environment.

<CodeBlock className="language-python">
{ContextManagerExample}
</CodeBlock>
Before triggering a reboot, persist any essential state externally (e.g., to the key-value store or dataset), as all in-memory data is lost after reboot. The example below tracks a reboot counter in the default key-value store and allows at most three restarts before exiting normally.

## Rebooting an Actor
<CodeBlock className="language-python">{RebootExample}</CodeBlock>

Sometimes, you want to restart your Actor to make it run from the beginning again. To do that, you can use the [`Actor.reboot`](../../reference/class/Actor#reboot) method. When you call it, the Apify platform stops the container of the run, and starts a new container of the same Actor with the same run ID and storages.
## Status message

Don't do it unconditionally, or you might get the Actor in a reboot loop.
[Status messages](https://docs.apify.com/platform/actors/development/programming-interface/status-messages) are lightweight, human-readable progress indicators displayed with the Actor run on the Apify platform (separate from logs). Use them to communicate high-level phases or milestones, such as "Fetching list", "Processed 120/500 pages", or "Uploading results".

<CodeBlock className="language-python">
{RebootExample}
</CodeBlock>
Update the status only when the user's understanding of progress changes - avoid frequent updates for every processed item. Detailed information should go to logs or storages (dataset, key-value store) instead.

## Actor status message
The SDK optimizes updates by sending an API request only when the message text changes, so repeating the same message incurs no additional cost.

To inform you or the users running your Actors about the progress of their runs, you can set the status message for the run, which will then be visible in the run detail in Apify Console, or accessible through the Apify API.
<CodeBlock className="language-python">{StatusMessageExample}</CodeBlock>

To set the status message for the Actor run, you can use the [`Actor.set_status_message`](../../reference/class/Actor#set_status_message) method.
## Conclusion

<CodeBlock className="language-python">
{StatusMessageExample}
</CodeBlock>
This page has presented the full Actor lifecycle: initialization, execution, error handling, rebooting, shutdown and status messages. You've seen how the SDK supports both context-based and manual control patterns. For deeper dives, explore the [reference docs](https://docs.apify.com/sdk/python/reference), [guides](https://docs.apify.com/sdk/python/docs/guides/beautifulsoup-httpx), and [platform documentation](https://docs.apify.com/platform).
21 changes: 21 additions & 0 deletions docs/02_concepts/code/01_class_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import asyncio

from apify import Actor


async def main() -> None:
async with Actor:
# Get input
actor_input = await Actor.get_input()
Actor.log.info('Actor input: %s', actor_input)

# Your Actor logic here
data = {'message': 'Hello from Actor!', 'input': actor_input}
await Actor.push_data(data)

# Set status message
await Actor.set_status_message('Actor completed successfully')


if __name__ == '__main__':
asyncio.run(main())
26 changes: 26 additions & 0 deletions docs/02_concepts/code/01_class_manual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import asyncio

from apify import Actor


async def main() -> None:
await Actor.init()

try:
# Get input
actor_input = await Actor.get_input()
Actor.log.info('Actor input: %s', actor_input)

# Your Actor logic here
data = {'message': 'Hello from Actor!', 'input': actor_input}
await Actor.push_data(data)

# Set status message
await Actor.set_status_message('Actor completed successfully')

finally:
await Actor.exit()


if __name__ == '__main__':
asyncio.run(main())
6 changes: 6 additions & 0 deletions docs/02_concepts/code/01_context_manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import asyncio

from apify import Actor


Expand All @@ -7,3 +9,7 @@ async def main() -> None:
Actor.log.info('Actor input: %s', actor_input)
await Actor.set_value('OUTPUT', 'Hello, world!')
raise RuntimeError('Ouch!')


if __name__ == '__main__':
asyncio.run(main())
13 changes: 13 additions & 0 deletions docs/02_concepts/code/01_error_handling_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import asyncio

from apify import Actor


async def main() -> None:
async with Actor:
# Any unhandled exception triggers Actor.fail() automatically
raise RuntimeError('Boom')


if __name__ == '__main__':
asyncio.run(main())
33 changes: 33 additions & 0 deletions docs/02_concepts/code/01_error_handling_manual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import asyncio
import random

from apify import Actor


async def do_work() -> None:
# Simulate random outcomes: success or one of two exception types.
outcome = random.random()

if outcome < 0.33:
raise ValueError('Invalid input data encountered')
if outcome < 0.66:
raise RuntimeError('Unexpected runtime failure')

# Simulate successful work
Actor.log.info('Work completed successfully')


async def main() -> None:
await Actor.init()
try:
await do_work()
except ValueError as exc: # Specific error mapping example
await Actor.fail(exit_code=10, exception=exc)
except Exception as exc: # Catch-all for unexpected errors
await Actor.fail(exit_code=91, exception=exc)
else:
await Actor.exit(status_message='Actor completed successfully')


if __name__ == '__main__':
asyncio.run(main())
6 changes: 6 additions & 0 deletions docs/02_concepts/code/01_init_exit.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import asyncio

from apify import Actor


Expand All @@ -14,3 +16,7 @@ async def main() -> None:
await Actor.fail(exit_code=91, exception=exc)

await Actor.exit()


if __name__ == '__main__':
asyncio.run(main())
27 changes: 27 additions & 0 deletions docs/02_concepts/code/01_instance_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import asyncio
from datetime import timedelta

from apify import Actor


async def main() -> None:
actor = Actor(
event_listeners_timeout=timedelta(seconds=30),
cleanup_timeout=timedelta(seconds=30),
)

async with actor:
# Get input
actor_input = await actor.get_input()
actor.log.info('Actor input: %s', actor_input)

# Your Actor logic here
data = {'message': 'Hello from Actor instance!', 'input': actor_input}
await actor.push_data(data)

# Set status message
await actor.set_status_message('Actor completed successfully')


if __name__ == '__main__':
asyncio.run(main())
32 changes: 32 additions & 0 deletions docs/02_concepts/code/01_instance_manual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import asyncio
from datetime import timedelta

from apify import Actor


async def main() -> None:
actor = Actor(
event_listeners_timeout=timedelta(seconds=30),
cleanup_timeout=timedelta(seconds=30),
)

await actor.init()

try:
# Get input
actor_input = await actor.get_input()
actor.log.info('Actor input: %s', actor_input)

# Your Actor logic here
data = {'message': 'Hello from Actor!', 'input': actor_input}
await actor.push_data(data)

# Set status message
await actor.set_status_message('Actor completed successfully')

finally:
await actor.exit()


if __name__ == '__main__':
asyncio.run(main())
20 changes: 18 additions & 2 deletions docs/02_concepts/code/01_reboot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import asyncio

from apify import Actor


async def main() -> None:
async with Actor:
# ... your code here ...
await Actor.reboot()
# Use the KVS to persist a simple reboot counter across restarts.
kvs = await Actor.open_key_value_store()
reboot_counter = await kvs.get_value('reboot_counter', 0)

# Limit the number of reboots to avoid infinite loops.
if reboot_counter < 3:
await kvs.set_value('reboot_counter', reboot_counter + 1)
Actor.log.info(f'Reboot attempt {reboot_counter + 1}/3')
# Trigger a platform reboot; after restart the code runs from the beginning.
await Actor.reboot()

Actor.log.info('Reboot limit reached, finishing run')


if __name__ == '__main__':
asyncio.run(main())
10 changes: 10 additions & 0 deletions docs/02_concepts/code/01_status_message.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import asyncio

from apify import Actor


async def main() -> None:
async with Actor:
await Actor.set_status_message('Here we go!')
# Do some work...
await asyncio.sleep(3)
await Actor.set_status_message('So far so good...')
await asyncio.sleep(3)
# Do some more work...
await Actor.set_status_message('Steady as she goes...')
await asyncio.sleep(3)
# Do even more work...
await Actor.set_status_message('Almost there...')
await asyncio.sleep(3)
# Finish the job
await Actor.set_status_message('Phew! That was not that hard!')


if __name__ == '__main__':
asyncio.run(main())
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ ignore = [
"PLR0911", # Too many return statements
"PLR0913", # Too many arguments in function definition
"PLR0915", # Too many statements
"PLR2004", # Magic value used in comparison, consider replacing `0.2` with a constant variable
"PTH", # flake8-use-pathlib
"PYI034", # `__aenter__` methods in classes like `{name}` usually return `self` at runtime
"PYI036", # The second argument in `__aexit__` should be annotated with `object` or `BaseException | None`
Expand Down
Loading