Developer

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;
  },
});
FieldNotes
nameCanonical 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
permissionScope required to call the method
params / returnsZod schemas
errorsTyped 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.