Remote APIs

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.

Mark a method as Remote

Markers are written in TypeScript on the method itself:

MarkerWhat it declares
@RemoteA 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.

Bind the marked methods to a service

A marked method becomes callable once its owner is bound:

What a call carries

Streaming calls

Results and failures

Values that cross

How calls and schemas are named

Questions, answered

Can a private or static method be marked as Remote?

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.

How do I export a Remote method under a different name?

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').

Why are rest parameters and default values refused?

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.

Why must the signal parameter be last?

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.

Can a stream method return a promise?

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>.

May an uplink item cross without a value?

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.

What is the difference between a direct call and a scoped call?

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.

Which values cannot cross the boundary?

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.

Why should I check the failure code instead of using instanceof?

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>/<reason> code and branch on that.

Which failure codes belong to the layer itself?

Three are universal: gateway/bad-request when a call does not satisfy the endpoint, and it can carry an issues list describing what was wrong; gateway/cancelled when the call was aborted; and gateway/internal for a failure the layer could not classify.