Skip to main content

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 one async 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 the await — 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 await inside a loop needs @bounded_loop and doesn’t count toward the limit.
  • Expose the generated resume handler at module level, like any other handler:
If you route runner callbacks through a single handler instead, 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 pass OPENAI_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

  1. Block N — your handler runs its initial segment: the job is submitted, escrow is locked, the continuation is persisted.
  2. Off-chain — selected runner(s) execute the job (LLM call, HTTP fetch, …).
  3. Block N+k — results are submitted and verified per the verification mode.
  4. Block N+k+1 — the callback fires your __resume segment with the result; settlement then pays the runner from escrow (see the job lifecycle for the finalization path).
Plan for latency of a few blocks, and remember each leg is a separate transaction with its own gas.

Further reading