Browse topics
On this page
Method Architecture
Every applet method has one canonical name and routes through three layers: frontend call, bundle sidecar wrapper, and the underlying command.
A method is the unit of work an applet exposes. It has exactly one canonical name everywhere: @<author>/<slug>.<verb> - one dot, one camelCase verb, no suffix, no alias. That single name appears in applet.json::methods[], in tools[], in the frontend call, and in the defineMethod registration.
defineMethod
A method is declared with defineMethod. Params and returns are Zod schemas, validated on both sides of the call.
import { defineMethod, z } from "@rightplace/applet-sdk/v2";
export const add = defineMethod({
name: "@my-org/notes.add",
scope: "resource", // "resource" (per resource) or "global" (singleton)
runIn: "backend", // "backend" (Node sidecar) or "frontend" (webview)
permission: "projectDb:write",
params: z.object({ title: z.string(), body: z.string() }),
returns: Note.schema,
errors: { NOTE_TOO_LONG: { max: z.number() } },
async run({ title, body }, ctx) {
const note = await Note.insert({ id: ctx.uuid(), title, body, createdAt: ctx.now() });
return note;
},
});
| Field | Notes |
|---|---|
name | Canonical applet-method name |
scope | "resource" binds to a mounted resource (ctx.resourceId set); "global" is a singleton and must be runIn: "backend" |
runIn | "frontend" runs in the webview; "backend" runs in the Node sidecar |
permission | Scope required to call the method |
params / returns | Zod schemas |
errors | Typed error codes, flat namespace (cannot collide with system codes) |
run(params, ctx) | The async handler; receives typed ctx |
The three layers
When a method is backed by a native command, it routes through three layers. This separation keeps the agent-facing name stable even when the implementation changes underneath.
Layer 1 (Frontend) ctx.methods["@my-org/notes.add"](params)
|
v
Layer 2 (Bundle sidecar) defineMethod registered as Handler::NodeSidecar
- Listed in tools[]
- Thin wrapper; body can be one line:
ctx.invoke("cmd_notes_add", params)
|
v
Layer 3 (Native command) cmd_notes_add
- Registered once in the host
- Keeps its original name; never renamed
- Never registered under the applet-method name
- Layer 1 is what callers use: the UI, other applets, the CLI, and agents all call
@<author>/<slug>.<verb>. - Layer 2 is the bundle sidecar wrapper. It always exists, even when its body is a single
ctx.invoke(...). It is the only layer that knows the applet’s bundle-local context and can transform params before calling down. - Layer 3 is the native command, when one is involved. It keeps its original name and is never exposed directly under the applet-method name.
Pure-TypeScript methods stop at Layer 2: the run body does the work directly using SDK primitives, with no Layer 3.
Calling a method
// from the frontend
const note = await ctx.methods["@my-org/notes.add"]({ title, body });
// from another applet (caller declares the target scope in its own applet.json)
await ctx.sdk.rp["my-org"].notes.add({ title, body });
// from the CLI
// rprp call '@my-org/notes.add' --args '{"title":"...","body":"..."}'
Error shape
Errors always arrive in a single wire shape:
{ error: { code: string, message: string, details?: unknown } }
Reserved system codes include HOOK_TIMEOUT, HOOK_PERMISSION_DENIED, HOOK_VALIDATION_FAILED, HOOK_NOT_FOUND, and HOOK_INTERNAL_ERROR. Your own error codes live in a flat namespace and cannot reuse those names.
Why one name
The applet-method name is the stable, agent-facing surface. Re-routing internally - a different underlying command, a different params shape - must not break callers. The wrapper absorbs those changes. That is why the name never carries a suffix and the native command is never registered directly under it.