> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cowboy.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# An actor's schedule

> Run a handler once after a delay, or on a repeating schedule, cancel it, and test it

An actor can run its own handlers later, once after a delay or over and over on a schedule, with nobody calling it. This page builds a reminders actor that does both, tests it without waiting for real time, and shows what happens when the money runs out.

First, [install the CLI](https://docs.cowboy.inc/getting-started/install.md).

## 1. Create the project

```bash theme={"system"}
cowboy init reminders --dir reminders
cd reminders
```

```text Output theme={"system"}
✓ Project initialized.
→ Preparing Python environment
✓ Python environment ready · .venv (0.1s)

Yeehaw! Project ready: /path/to/reminders
```

## 2. Write the actor

Replace `actors/reminders/main.py` with:

```python theme={"system"}
from cowboy_sdk import HOUR, OWNER, SELF, Actor, callable_by, every, public, runtime


class RemindersActor(Actor):
    @public
    def init(self, payload: bytes | None = None) -> None:
        self.storage["reminders"] = {}
        self.storage["checks"] = 0

    @callable_by(OWNER)
    def remind(self, request: object) -> None:
        if not isinstance(request, dict):
            raise ValueError('send {"text": ..., "after": seconds}')
        text = request["text"]
        reminders = self.storage["reminders"]
        if text in reminders:
            raise ValueError("already set: forget it first")
        timer_id = runtime.schedule_after(
            int(request["after"]), self.fire, text.encode()
        )
        reminders[text] = timer_id.hex()
        self.storage["reminders"] = reminders

    @callable_by(OWNER)
    def forget(self, text: object) -> None:
        reminders = self.storage["reminders"]
        timer_id = reminders.pop(text, None)
        if timer_id is not None:
            runtime.cancel_timer(bytes.fromhex(timer_id))
        self.storage["reminders"] = reminders

    @callable_by(SELF, decode="raw")
    def fire(self, payload: bytes) -> None:
        text = payload.decode()
        reminders = self.storage["reminders"]
        reminders.pop(text, None)
        self.storage["reminders"] = reminders
        runtime.emit_event("reminder", {"text": text})

    @callable_by(OWNER)
    def start_checks(self, _payload: object) -> None:
        every.start(self.check)

    @callable_by(OWNER)
    def stop_checks(self, _payload: object) -> None:
        every.stop(self.check)

    @every(6 * HOUR)
    def check(self) -> None:
        self.storage["checks"] = self.storage["checks"] + 1
```

### Once, after a delay

`runtime.schedule_after(seconds, handler, payload)` runs `handler` once, `seconds` from now, with `payload` as its raw bytes. Here, `remind` schedules `fire` that way. `fire` is `@callable_by(SELF)`, which means only the actor itself can call it, and a timer runs as the actor. `decode="raw"` hands it the payload as bytes.

`schedule_after` returns the timer's id, and `runtime.cancel_timer(timer_id)` stops a timer that hasn't run. That's why `remind` keeps each id: so `forget` can cancel it.

Delays are in seconds, which you can write with `MINUTE`, `HOUR`, `DAY` and `WEEK`. They can reach up to about 30 days ahead, and longer ones are refused.

### On a schedule

`@every(period)` makes a handler run every `period` seconds, once `every.start` starts it. `every.start(self.check)` starts the schedule, or restarts it if it is running, and the first run comes one full period later. `every.stop(self.check)` ends it.

Only the schedule runs the handler. A call from anyone, the owner included, is refused, so nobody can run it early. A run that raises undoes its own changes and keeps the schedule.

## 3. Test it

Replace `tests/test_reminders.py` with:

```python theme={"system"}
import pytest
from cowboy_sdk import DAY, PermissionDeniedError, codec, mock_host
from cowboy_sdk.testing import SimulatedChain

from actors.reminders import main


def deploy() -> tuple[main.RemindersActor, SimulatedChain]:
    actor = main.RemindersActor()
    chain = SimulatedChain(actor)
    mock_host.seed_actor_owner(mock_host.context()["sender"])
    chain.call(actor.init)
    return actor, chain


def test_a_reminder_fires_once_after_its_delay() -> None:
    actor, chain = deploy()
    chain.call(actor.remind, codec.encode({"text": "Call Ana", "after": 600}))
    assert chain.advance_block(599) == []
    fired = chain.advance_block(1)
    assert [handler for handler, _ in fired] == ["fire"]
    name, payload = chain.events()[-1]
    assert (name, codec.decode(payload)) == ("reminder", {"text": "Call Ana"})


def test_the_same_reminder_cannot_be_set_twice() -> None:
    actor, chain = deploy()
    chain.call(actor.remind, codec.encode({"text": "Call Ana", "after": 600}))
    with pytest.raises(ValueError):
        chain.call(actor.remind, codec.encode({"text": "Call Ana", "after": 60}))


def test_a_forgotten_reminder_never_fires() -> None:
    actor, chain = deploy()
    chain.call(actor.remind, codec.encode({"text": "Call Ana", "after": 600}))
    chain.call(actor.forget, codec.encode("Call Ana"))
    chain.advance_block(1200)
    assert chain.events() == []


def test_nobody_can_fire_a_reminder_early() -> None:
    actor, chain = deploy()
    chain.set_sender(b"\x33" * 20)
    with pytest.raises(PermissionDeniedError):
        chain.call(actor.fire, b"Call Ana")


def test_checks_run_every_six_hours_until_stopped() -> None:
    actor, chain = deploy()
    chain.call(actor.start_checks)
    chain.advance_block(DAY)
    assert codec.decode(chain.state("checks")) == 4
    chain.call(actor.stop_checks)
    chain.advance_block(DAY)
    assert codec.decode(chain.state("checks")) == 4


def test_nobody_can_run_a_check_early() -> None:
    actor, chain = deploy()
    chain.call(actor.start_checks)
    with pytest.raises(PermissionDeniedError):
        chain.call(actor.check)
```

```bash theme={"system"}
cowboy test
```

```text Output theme={"system"}
tests/test_reminders.py ......                                           [100%]
============================== 6 passed in 0.14s ===============================
```

The tests don't wait for real time. `chain.advance_block(n)` moves time forward `n` blocks and runs every timer that comes due, where a block is the network's unit of about one second.

## 4. Run it on your machine

```bash theme={"system"}
cowboy run --handler init
cowboy run --handler remind --payload-json '{"text": "Call Ana", "after": 600}'
```

```text Output theme={"system"}
Timers:
  at block 601 -> fire 0x00434455450000018bcfee8fc043616c6c20416e61
```

The second run set a timer that comes due at block 601, 600 blocks from now. Once the actor is deployed, `cowboy actor timers <actor>` shows its timers.

## Who pays, and when the money runs out

The actor pays for each run. If its balance can't cover one, the timer is removed and the handler doesn't run. That also ends an `@every` schedule, because each run sets the next one.

Scheduled runs come only from the actor's balance, so keep it funded: `cowboy actor fund` adds money, and `cowboy actor auto-topup` refills it automatically ([Deploy and operate](https://docs.cowboy.inc/build/deploy.md#keep-it-funded)). After a top-up, call `start_checks` again.

## Next

* [Keep an actor safe](https://docs.cowboy.inc/build/sdk/safety.md)
* [SDK reference: `schedule_after`](https://docs.cowboy.inc/developers/sdk-reference#schedule_after)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.