Custom services
A connection adapter can expose operations that are not files, a console or settings — reading a sensor, restarting a service, querying a device API. Apps reach them through client.services, with a permission per service.
Availability
Declare the service you need in your manifest as services.<service-id>, for example:
{ "permissions": ["services.acme.sensor"] }That grants every method the acme.sensor service advertises. The service must be present on the current workspace connection: without it, calls fail with unavailable rather than silently doing nothing.
Service identifiers are lowercase, dot-separated, and may not start with system, files, console, host, terminal or services — those namespaces belong to the desktop.
Interface
client.services.list(signal?: AbortSignal): Promise<readonly ServiceMethodInfo[]>;
client.services.call(method: string, params?: Json, signal?: AbortSignal): Promise<Json>;Custom entries appear in list() alongside built-in methods, with the service's permissions, whether they are granted, whether they are available, and a source identifying which connection provides them. Method names are always <service-id>.<method>.
The adapter defines the parameters and the result. Both are plain JSON, and validating the result is your app's job — the desktop checks identity and permissions, not meaning.
Examples
Discover before calling:
const methods = await client.services.list();
const sensor = methods.find((m) => m.name === "acme.sensor.read");
if (!sensor?.granted) return hide(); // not permitted for this app
if (!sensor.available) return showOffline(); // adapter missing or disconnectedCall it, with a timeout of your own:
import { RpcError } from "@techartdev/shellcanvas-app-sdk";
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
const result = await client.services.call("acme.sensor.read", { probe: "inlet" }, controller.signal);
const value = (result as { celsius?: number }).celsius;
if (typeof value !== "number") throw new Error("Unexpected adapter response.");
render(value);
} catch (error) {
if (error instanceof RpcError && error.code === "unavailable") showOffline();
else throw error;
} finally {
clearTimeout(timer);
}Re-run discovery when the environment changes: switching or replacing a connection can add or remove services.
client.events.subscribe((batch) => {
if (batch.events.some((e) => e.topic === "system.services")) void refreshServiceButtons();
});Errors
| Code | Message | Meaning |
|---|---|---|
invalid | Provide a custom service method and JSON parameters | Malformed call. |
denied | This app needs the services.<service> permission | Not declared, or not approved. |
unavailable | This workspace does not expose that service method | No adapter provides it here. |
unavailable | The selected service is unavailable | Present but not usable right now. |
unavailable | The accepted workspace connection is unavailable | The workspace is disconnected. |
aborted | Service call canceled | Your signal fired. |
closed | Service connection changed; the remote outcome may be uncertain. Inspect before retrying. | A replacement happened mid-call. |
Anything the adapter itself reports arrives with the adapter's own code and message.
Lifecycle and permissions
While a workspace is disconnected, list() still returns the services it knows about with available: false, so your interface keeps its shape instead of emptying and refilling.
A granted service permission covers every method that service advertises — there is no per-method approval — so scope matters when you ask for it. It grants nothing else: no files, no console, no network.
Adapters are native programs the user installed and trusted. Their operations run with the user's operating-system permissions, and an uncertain outcome means exactly that: inspect the device before retrying an action that may have taken effect.
Today no production adapters ship with ShellCanvas: SSH provides the built-in services, and custom services come from adapters a user installs. Build against discovery, so your app degrades cleanly where a device is absent.
Related guides
- Build your first adapter — the other side of this interface.
- Adapter services — what an adapter can advertise.
- Events and environment API — reacting to services appearing and disappearing.
- Extension permissions and trust — how service permissions are shown to users.