A plugin package can expose methods that code on the other side calls directly across the boundary between the host and its clients: mark the method as Remote, bind its owner to a service, and the runtime carries the calls, the streams and the cancellation between the two sides. This page is the authoring reference for that layer: how methods are declared and bound, what a call carries and returns, and the value and naming rules every call is validated against.
The end-to-end walkthrough of the same model is on Remote calls for extension authors; the messages a running client exchanges with the host are on The Oda Remote API; and what a build generates for a Remote-capable package is on Plugin artifacts and the catalog.
Markers are written in TypeScript on the method itself:
| Marker | What it declares |
|---|---|
@Remote | A direct call, under the method's own name. |
@Remote('name') | A direct call, exported under another name. |
@Remote({ mode: 'stream' }) | A streaming call. |
@RemoteScope('context') | A context-scoped call, whose receiver comes from the named context, with an optional second name. |
Remote decorators require a public instance method with a string name.Remote method "<method>" has conflicting invocation markers; keep the single marker that says how the method is called.{ mode: 'stream' }; anything else fails with Remote options must contain exactly mode: "stream".T | undefined still counts as required; only an optional parameter may be left out of a call.A marked method becomes callable once its owner is bound:
1. A descriptor under another version is refused, and the markers read back in declaration order.ctx. invocation describes the call in progress: the service it reached, the peer that made it and the signal. Outside a Remote call the property reads as undefined.signal: AbortSignal as its final parameter; the runtime injects the signal, and it never travels as a sent argument.@Remote({ mode: 'stream' }) and returns an Iterable<Out>, an AsyncIterable<Out> or a RemoteStream<Out, In>; each item travels on the logical stream as the method produces it. Another return shape, a promise included, is refused with stream Remote methods must return Iterable<Out>, AsyncIterable<Out>, or RemoteStream<Out, In>.send(item), end() and dispose(): send adds one item to the caller's side of the stream, end half-closes it so no further items follow, and dispose releases the handle when the caller is done.ctx. invocation. uplink(); it is available once per call, and asking for it a second time throws.{ ok: true, value } on success or { ok: false, error } on failure.RemoteError carrying a stable code of the form <domain>/ <reason>.gateway/ bad-request when the call does not satisfy the endpoint, and that failure can carry an issues list describing what was wrong; gateway/ cancelled when the call was cancelled; and gateway/ internal for a failure the layer could not classify.code.null, strings, booleans, finite numbers, plain objects with enumerable fields and dense arrays all cross.NaN, infinities, -0, class instances and other non-plain objects, symbol keys, non-enumerable fields, sparse arrays and cycles are refused wherever they would cross.undefined crosses without one, while nested undefined, NaN and infinities stay refused.Uint8Array data crosses only as the single result of a unary call, directly or nested inside it; parameters, events and stream items stay JSON-only, and another use is refused with Remote Uint8Array is only supported in unary results.<namespace>/ <method>; a registered invocation carries the full id <package>#<namespace>/ <method>.<package>#<name>, and a package's contribution per side as <package>#<face>, where the face is host or client._, $, . and -. A lone . or . . is refused, and # is refused inside a name because it separates the package part of an identity; a name outside the set is refused with <subject> must contain only RPC endpoint segment characters.No. A marker needs a public, non-static instance method with a string name and a concrete implementation; private or static methods, methods without an implementation and generic methods are refused, with Remote decorators require a public instance method with a string name. Mark a public instance method instead.
Write the name in the marker: @Remote('name') exports the call under name while the method keeps its own name on the class. A context-scoped marker takes an optional second name for the call the same way, as in @RemoteScope('context', 'name').
The layer keeps a call to exactly what the method declares, and only plain named bindings can be declared: a rest parameter, a destructured binding or a default value is refused. Write the parameters out one by one, and make a parameter optional where a call may leave it out; a parameter written as T | undefined still counts as required.
Because the signal is injected, not sent: the runtime provides it at call time, and the check requires the declaration to end with it, refusing anything else with cancellation signal must be the final parameter. Declare signal: AbortSignal as the method's last parameter and react to it when the caller goes away.
No. A streaming method must return an Iterable<Out>, an AsyncIterable<Out> or a RemoteStream<Out, In>; anything else, a promise included, is refused with stream Remote methods must return Iterable<Out>, AsyncIterable<Out>, or RemoteStream<Out, In>.
Yes, at the top level only. An item sent as undefined crosses as an item with no value; nested undefined, NaN and infinities inside an item stay refused.
A direct call reaches the method on the bound service, under its own name or the name the marker gave it. A scoped call, declared with @RemoteScope('context'), takes its receiver from the named context instead, with an optional second name for the call; the same segment rules apply to the context key and the name.
The refused set is shared on every side: NaN, infinities, -0, class instances and other non-plain objects, symbol keys, non-enumerable fields, sparse arrays and cycles. What crosses is null, strings, booleans, finite numbers, plain objects with enumerable fields and dense arrays; bytes cross only as the single result of a unary call.
Because an instance check does not survive a bundle boundary: a failure is recognised structurally, not by its class, and discrimination is always by the code. Read the stable <domain>/ code and branch on that.
Three are universal: gateway/ when a call does not satisfy the endpoint, and it can carry an issues list describing what was wrong; gateway/ when the call was aborted; and gateway/ for a failure the layer could not classify.