Skip to main content

Overview

The cowboy_sdk is a Python library that provides the canonical developer experience for writing Cowboy actors. It ships with the PVM and lives at node/pvm/Lib/cowboy_sdk/ in the monorepo. On deployed actors, sys.path already includes it — you can import cowboy_sdk directly.
The SDK is specified by CIP-6. APIs may still evolve; see the CIP for the authoritative surface.

What the SDK Gives You

FeatureModulePurpose
@actor decoratorcowboy_sdk.actorInjects self.storage, self.address, and wires CBOR auto-serialization
runner.continuation FSMcowboy_sdk.runnerPause an actor mid-handler while a runner job is in flight; resume on callback
CowboyModelcowboy_sdk.modelsTyped, CBOR-serializable structured data
capture()cowboy_sdk.continuationExplicitly declare variables preserved across await
codeccowboy_sdk.codecCBOR encode/decode with deterministic ordering
runtimecowboy_sdk.runtimeAccess to block context, gas, events, logging
SoftFloatcowboy_sdk.typesDeterministic floating-point type
mock_hostcowboy_sdk.mock_hostStub the PVM host for unit tests off-chain

Two Ways to Write an Actor

Low-level (host API): import pvm_host and manage bytes serialization yourself. Used by actors/feed-subscriber/main.py.
import pvm_host

def increment(ctx, payload):
    val = pvm_host.get_state(b"counter") or b"\x00" * 8
    n = int.from_bytes(val, "big") + 1
    pvm_host.set_state(b"counter", n.to_bytes(8, "big"))
    return n
Recommended — CIP-6 SDK:
from cowboy_sdk import actor, public, runner, capture, CowboyModel

class ChatEntry(CowboyModel):
    id: int
    sender: str
    message: str

@actor
class Counter:
    @public
    def init(self, payload):
        self.storage["count"] = 0
        return b"ok"

    @public
    def increment(self, payload):
        self.storage["count"] = (self.storage.get("count") or 0) + 1
        return self.storage["count"]
The @actor decorator auto-CBOR-encodes values written to self.storage, injects self.address, and registers handlers with the PVM. Handlers take a payload argument, are deny-by-default (@public opens them), and one-time setup belongs in the init handler (invoked at deploy) — not in __init__, which runs on every dispatch. CowboyModel fields are schema-checked and serialize deterministically.

Continuation FSM for Off-Chain Jobs

Calling a runner (LLM/HTTP/MCP) is asynchronous — the result arrives in a later block as a callback transaction. The SDK compiles @runner.continuation methods into a synchronous state machine under the hood so you can write code that looks sequential:
from cowboy_sdk import actor, runner, capture

@actor
class Chat:
    @runner.continuation
    async def chat(self, msg):
        ctx = capture()      # declare what survives across the await
        ctx.msg = msg
        result = await runner.llm(msg)
        self.storage[f"reply:{ctx.msg}"] = str(result)
        return b"ok"
The compiler splits the method at each await, persists captured variables, and resumes on the callback. See CIP-6 §10 for the full rule set (await point limits, non-reversible send, etc.).

Unit Testing

Use cowboy_sdk.testing.SimulatedChain (an in-memory harness over cowboy_sdk.mock_host) to run actors under pytest without a live chain:
from cowboy_sdk.testing import SimulatedChain

import my_actor

def test_increment():
    chain = SimulatedChain(my_actor.Counter())
    chain.call("init")
    chain.call("increment")
    chain.call("increment")
    chain.assert_state("count", b"\x02")  # raw stored bytes (CBOR-encoded 2)

Where to Find More

  • Source: node/pvm/Lib/cowboy_sdk/ — read the code directly
  • Spec: CIP-6 — normative API
  • Examples: node/examples/llm_chat/, node/examples/ring-demo/, node/examples/token/ — end-to-end actors
  • Actor anatomy: Minimal Actor
  • Transaction format: tx-format
  • Best practices: best-practices