Your First Actor¶
In this guide you'll build a traffic light — a state machine that cycles through green, yellow, and red. Along the way you'll learn the three core ideas in Casty: messages, behaviors, and state transitions.
Messages¶
Every actor communicates through messages. In Casty, messages are frozen dataclasses — immutable values that are safe to send between actors.
Our traffic light handles two messages:
@dataclass(frozen=True)
class Tick:
pass
@dataclass(frozen=True)
class GetColor:
reply_to: ActorRef[str]
type TrafficLightMsg = Tick | GetColor
Tick advances the light to the next color. GetColor is a request-reply message: the sender includes its own ActorRef so the light can respond.
Behaviors as State¶
Instead of a single handler with an if color == "green" check, each color is its own behavior function. The traffic light is whichever behavior is currently active:
def green() -> Behavior[TrafficLightMsg]:
async def receive(
ctx: ActorContext[TrafficLightMsg], msg: TrafficLightMsg
) -> Behavior[TrafficLightMsg]:
match msg:
case Tick():
print("🟢 → 🟡")
return yellow()
case GetColor(reply_to):
reply_to.tell("green")
return Behaviors.same()
return Behaviors.receive(receive)
def yellow() -> Behavior[TrafficLightMsg]:
async def receive(
ctx: ActorContext[TrafficLightMsg], msg: TrafficLightMsg
) -> Behavior[TrafficLightMsg]:
match msg:
case Tick():
print("🟡 → 🔴")
return red()
case GetColor(reply_to):
reply_to.tell("yellow")
return Behaviors.same()
return Behaviors.receive(receive)
def red() -> Behavior[TrafficLightMsg]:
async def receive(
ctx: ActorContext[TrafficLightMsg], msg: TrafficLightMsg
) -> Behavior[TrafficLightMsg]:
match msg:
case Tick():
print("🔴 → 🟢")
return green()
case GetColor(reply_to):
reply_to.tell("red")
return Behaviors.same()
return Behaviors.receive(receive)
Notice: there are no mutable variables. No self.color = "yellow". When the light receives a Tick, it returns a different behavior — and that is the state transition. This is called behavior recursion.
Running It¶
The main() function spawns the actor and sends messages:
async def main() -> None:
async with ActorSystem() as system:
light = system.spawn(green(), "traffic-light")
light.tell(Tick()) # green → yellow
light.tell(Tick()) # yellow → red
light.tell(Tick()) # red → green
await asyncio.sleep(0.1)
color = await system.ask(
light, lambda r: GetColor(reply_to=r), timeout=5.0
)
print(f"Current color: {color}") # green
asyncio.run(main())
Output:
Run the Full Example¶
git clone https://github.com/gabfssilva/casty.git
cd casty
uv run python examples/guides/01_your_first_actor.py
What you learned:
- Messages are frozen dataclasses — immutable and type-safe.
- Behaviors are values, not classes. Each state is a function that returns a
Behavior. - State transitions happen by returning a different behavior — no mutation needed.
- Request-reply works by including an
ActorRefin the message.