Overview
Actors are deterministic and cannot reach the outside world directly. To call an LLM, fetch a URL, or invoke an MCP tool, an actor submits a job to the Runner network (CIP-2): staked off-chain nodes execute the job, results are verified, and the actor resumes with the result a few blocks later. This guide covers the developer-facing SDK. For the protocol mechanics — runner selection, verification, payment — see the off-chain compute architecture pages.The continuation model
A job round-trip spans multiple blocks, but you write it as oneasync function:
@runner.continuation compiles the function at import time into a finite state machine (FSM): the code before the await becomes the initial handler (submit the job, persist the continuation), and the code after it becomes a generated ask__resume handler invoked by the runner callback. The await never executes at runtime — awaiting a runner.* task outside a @runner.continuation function raises an error.
Rules that follow from the FSM model:
capture()is mandatory for any local variable you need after theawait— assign it to the returned context object (ctx.question = ...). Uncaptured locals are gone when the resume segment runs.- At most 8 sequential await points per function (static check at compile time); an
awaitinside a loop needs@bounded_loopand doesn’t count toward the limit. - Expose the generated resume handler at module level, like any other handler:
runner.handle_runner_result(self, msg) dispatches the message to the right __resume method based on its reply_handler field.
Job types
Jobs can be chained — each
await is a separate job:
Verification modes
Off-chain results enter consensus through a verification mode declared in the job spec (CIP-2 §9). Which one to pick:
Runners whose results fail verification are penalized — reputation always, and stake slashing in protocol-defined cases. The SDK’s high-level calls fill sensible defaults; the underlying job spec also carries resource bounds (output and wall-time caps), a
max_price/tip in CBY, and a timeout in blocks, after which the job can be re-assigned.
Secrets
API keys never go in actor state (chain state is public). Runners obtain credentials out-of-band: on a contributor-run local devnet you passOPENAI_API_KEY / ANTHROPIC_API_KEY to the runner process environment; in production, secrets live encrypted in the Secrets Manager and are released to attested runners (CIP-24).
Worked example
In a full source checkout, the LLM chat example is the canonical end-to-end flow:llm_actor.py implements a chat actor whose chat handler is a @runner.continuation (capture → await runner.llm(...) → store the response and emit an event), with module-level chat / chat__resume entrypoints and a callback router using runner.handle_runner_result. Its helper script boots a validator plus a runner with API keys and drives a conversation.
Timeline of one job
- Block N — your handler runs its initial segment: the job is submitted, escrow is locked, the continuation is persisted.
- Off-chain — selected runner(s) execute the job (LLM call, HTTP fetch, …).
- Block N+k — results are submitted and verified per the verification mode.
- Block N+k+1 — the callback fires your
__resumesegment with the result; settlement then pays the runner from escrow (see the job lifecycle for the finalization path).
Further reading
- Off-chain compute overview — components and trust model
- Job lifecycle — phase-by-phase protocol walkthrough
- Callbacks, timeouts & challenges — failure paths
- CIP-2 — authoritative spec

