Skip to main content

Overview

This tutorial walks through the full development loop for a Cowboy Actor: scaffold a project, write a counter actor with the cowboy_sdk Python library, unit-test it off-chain, deploy it to a local devnet, and call it from the CLI. The Quickstart deploys a pre-built starter actor; this tutorial is about writing your own. If you haven’t booted a devnet yet, do steps 1–2 of the Quickstart first — this page assumes a validator is reachable at http://localhost:4000 and the cowboy CLI is on your PATH. What you’ll learn:
  • The shape of an SDK actor: @actor, handlers, self.storage
  • Handler modes (@pure vs @deferred) and permissions (@public)
  • How to unit-test an actor before deploying it
  • The deploy → execute → inspect → iterate loop

1. Scaffold a project

mkdir counter-tutorial && cd counter-tutorial
cowboy init local
This creates a .cowboy/ directory with a funded devnet wallet and a config pointing at http://localhost:4000 (see cowboy init). Create a directory for your actor:
mkdir -p actors/counter

2. Write the actor

Create actors/counter/main.py:
"""A counter actor: increment a value, read it back, emit events."""

from cowboy_sdk import actor, public, pure, runtime, codec


@actor
class Counter:
    @public
    def init(self, payload):
        self.storage["count"] = 0
        return b'{"status":"initialized"}'

    @pure
    @public
    def get(self, payload):
        return codec.encode(self.storage.get("count", 0))

    @public
    def increment(self, payload):
        count = int(self.storage.get("count", 0)) + 1
        self.storage["count"] = count
        runtime.emit_event("counter.incremented", {"count": count})
        return codec.encode(count)


# Module-level entrypoints: the PVM dispatches to top-level functions by name.
def _get_actor():
    return Counter()


def init(payload):
    return _get_actor().init(payload)


def get(payload):
    return _get_actor().get(payload)


def increment(payload):
    return _get_actor().increment(payload)

What each piece does

PiecePurpose
@actorClass decorator that injects self.storage (persistent key/value state) and self.address, and wires handler dispatch
self.storage["count"]Dict-like persistent state; values are CBOR-encoded automatically and metered in Cells
@publicPermission decorator — handlers are deny-by-default; @public lets anyone call this handler
@pureDeclares that the handler issues no asynchronous side effects (no send(), job submissions, or timers); state reads/writes and synchronous call() are allowed. Handlers that need async operations use @deferred instead
runtime.emit_event(name, payload)Emits an event (string, bytes, or dict payload) visible in actor logs
codec.encode(...)Canonical CBOR encoding — the wire format for everything crossing a trust boundary
Module-level wrappersRequired for on-chain dispatch: the PVM invokes top-level functions by handler name
The init handler runs once at deploy time (the deploy flow invokes the handler named init by default). It is a normal handler, not Python’s __init__.

Determinism rules you’ll hit first

Actor code must be deterministic — every validator replays it and must get identical results. The SDK ships replacements for the usual suspects:
  • import time is forbidden → use runtime.get_block_height() / runtime.get_timestamp_ms() (block-derived time)
  • import random is forbidden → use runtime.randomness(domain) (protocol randomness)
  • pickle is forbidden → use cowboy_sdk.codec (CBOR)
  • set() iteration order is not deterministic — prefer cowboy_sdk.ordered_set when you iterate over a set
See Determinism & Sandbox for the full list.

3. Test it off-chain

The SDK includes SimulatedChain, an in-memory harness that stubs the PVM host so handlers run as plain Python. Create actors/counter/test_counter.py:
from cowboy_sdk.testing import SimulatedChain

import main


def test_increment():
    chain = SimulatedChain(main.Counter())
    chain.call("init")
    chain.call("increment")
    chain.call("increment")

    result = chain.call("get")
    assert b"\x02" in result  # CBOR-encoded 2

    events = chain.events()
    assert events[-1][0] == "counter.incremented"


def test_determinism():
    def scenario(chain):
        chain.call("init")
        chain.call("increment")

    chain = SimulatedChain(main.Counter())
    assert chain.check_determinism(scenario)
Run it with pytest. Put only the cowboy_sdk package on your PYTHONPATHnode/pvm/Lib as a whole is the PVM’s Python standard library and will shadow CPython’s stdlib if you add the entire directory:
mkdir -p .sdkpath && ln -s <monorepo>/node/pvm/Lib/cowboy_sdk .sdkpath/cowboy_sdk
PYTHONPATH=.sdkpath pytest actors/counter/test_counter.py
For the full local-testing toolkit — mocking cross-actor calls, advancing blocks to fire timers, state snapshots — see Testing Actors Locally.

4. Deploy

cowboy actor deploy --code actors/counter/main.py --salt 0x01
The salt is a hex string (up to 32 bytes) that lets you deploy the same code to distinct addresses. You’ll get a transaction hash and a deterministic actor address (derived from your address, the code, and the salt — compute it ahead of time with cowboy actor address). Save it:
export COUNTER=<ACTOR_ADDR>

5. Call it

# Mutate state
cowboy actor execute --actor $COUNTER --handler increment --payload 0x

# Read state
cowboy actor execute --actor $COUNTER --handler get --payload 0x
Inspect the actor and its events:
cowboy actor get --address $COUNTER
cowboy actor logs --address $COUNTER
You should see your counter.incremented events in the logs.

6. Iterate

Edit main.py, re-run the tests, and redeploy. Deploying with a new salt (e.g. --salt 0x02) gives you a fresh actor at a new address; to upgrade the code at an existing address, see cowboy upgrade-actor. A natural next step: schedule the counter to increment itself. Actors can register block-height timers — see the worked example in examples/21-pure-timer-scheduler/ and the Scheduler overview.

Next steps

Testing Actors Locally

SimulatedChain, CallMock, and determinism checks in depth

Submitting Jobs to Runners

Call LLMs, HTTP APIs, and MCP tools from your actor

Working with Tokens

Create and move CIP-20 fungible tokens from actors and the CLI

SDK Overview

The full cowboy_sdk surface: models, continuations, guards