4. Runnables & API
Expose capabilities through one declaration. The kernel generates REST, MCP, and CLI bindings.
The Gap
So far, other Python code calls checker.check() directly. But what if you want it callable via REST API? Or as an MCP tool? Or as a CLI command? Without the kernel, you’d write three implementations. With the kernel: declare it once.
The bus is the kernel’s capability hub — a named dispatch table of callables ("checker.check", "config.get", …). Any component holding a reference to the kernel can invoke(name, params) by string name. @runnable is how you put a method on the bus; transport adapters (REST/MCP/CLI) read from the bus to expose it externally. Everything in this tutorial — and most of what follows — is about the bus.
@runnable
from pydantic import BaseModel
from signalpy.kernel import runnable
class CheckParams(BaseModel):
text: str
language: str = "EN"
@component("checker")
class SpellChecker:
@runnable("check", params=CheckParams, description="Check spelling")
async def check_text(self, params):
misspelled = [w for w in params.text.split()
if not self.by_language[params.language].check_word(w)]
return {"misspelled": misspelled}@runnable puts a callable on the bus. It has a name ("check"), a typed param schema (CheckParams), and a description. Both def and async def work.
Bus Invocation
Any component (or the main script) can call it:
result = await kernel.bus.invoke("checker.check", {
"text": "hello wrld", "language": "EN",
})
# → {"misspelled": ["wrld"]}From inside a component, use self.rt.invoke() — same thing, but policy-checked.
@api — Transport Surfaces
To expose runnables via REST, MCP, or CLI:
@component("checker")
@api("rest", prefix="/spell", version="v1") # → POST /api/v1/spell/check
@api("mcp", name="spell-tools") # → MCP tool "checker.check"
@api("cli", prefix="spell") # → spell check --text "hello"
class SpellChecker:
@runnable("check", params=CheckParams, description="Check spelling")
async def check_text(self, params): ...The component never knows how it’s called. @runnable puts a capability on the bus. @api tells transport adapters — components that translate bus calls into REST/MCP/CLI traffic, covered in tutorial 5 — to expose it. REST endpoints, MCP tools, and CLI commands are generated from the same declaration.
Internal Runnables
Some operations should be on the bus but not exposed via API:
@runnable("_warmup", params=BaseModel, internal=True, description="Warm cache")
async def warmup(self, params): ...internal=True keeps it off all API surfaces. Still callable via kernel.bus.invoke("checker._warmup").
Typed Params
Params are Pydantic models. They give you validation, defaults, auto-generated schemas, and attribute access (params.text, not params["text"]).