Skip to content

App capabilities

import * as AppCapabilities from '@dxos/app-toolkit/AppCapabilities';
import * as AppCapability from '@dxos/app-toolkit/AppCapability';
import * as AppSurface from '@dxos/app-toolkit/AppSurface';
import * as UiHooks from '@dxos/react-ui/Hooks';

The framework defines how plugins extend each other, but not what an app is made of. @dxos/app-toolkit adds that for a workspace app such as Composer: a layout, App Graph, settings, translations, the standard surface roles. Its capabilities are in AppCapabilities, and Composer and any other app built with the toolkit read them. AppCapability has a module maker for most of them, which sets the activation event and environments so a manifest only names the file.

Most plugins touch a handful of these. The rest exist for the plugins that build the app’s own features, and a plugin that is not one of those can ignore them.

  • Layout. Layout is the current layout as an atom: what is open, the sidebar, the mode. Plugins read it; the layout plugin provides it. Changing it goes through LayoutOperation.
  • App Graph. AppGraphBuilder adds nodes and actions, AppGraph is the graph itself for reading, and AppGraphRetention keeps nodes loaded while they are off screen. App Graph covers them.
  • Settings and text. Settings adds a section to the settings page, and Translations gives the plugin’s strings their text. Settings and state covers settings, and Translations below covers text.
  • Schema. Schema registers the ECHO types the app can store, query and name. Using ECHO in a plugin covers it.
  • Agents. SkillDefinition gives the assistant a skill, Toolkit adds tools outside a skill, and McpServer exposes a built-in MCP server.
  • Files and content. FileUploader stores an uploaded file, Exporter adds an export format, TextContent extracts an object’s text for search and agents, and PluginAsset publishes a file other tools can read, such as the plugin’s spec.
Capability Maker What it adds
AppGraph none The graph itself, for reading.
AppGraphBuilder appGraphBuilder Nodes and actions in App Graph.
AppGraphRetention none Nodes App Graph keeps loaded.
Exporter exporter An export format for objects.
FileUploader none Storage for files people add.
Layout none The current layout, as an atom.
McpServer mcpServer A built-in MCP server.
PluginAsset pluginAsset A file other tools can read, such as the plugin’s spec.
Schema schema ECHO types the app can store, query and name.
Settings settings A section in the settings page.
SkillDefinition skillDefinition A skill the assistant can use.
SpaceTemplate spaceTemplates A template for new spaces.
TextContent textContent The text of an object, for search and agents.
Toolkit none Tools for the assistant outside a skill.
Translations translations Text for the plugin’s strings and type labels.

AppCapability.surface and AppCapability.operationHandler make modules for the framework’s ReactSurface and OperationHandler capabilities.

A plugin never writes its UI text inline. It contributes translation resources, keyed by language and namespace, and reads strings by key, so the app can show them in the person’s language and other plugins can label its objects.

A resource maps a language to namespaces, and each namespace to keys and their text. Use the plugin’s key as the namespace for its own strings, and a type’s typename for the strings that describe the type. AppCapability.translations contributes the resources; it activates at startup, since labels are needed for the first screen.

src/translations.ts
import {
import Type
Type
} from '@dxos/echo';
import type * as
import Theme
Theme
from '@dxos/react-ui/Theme';
import {
const StashNamespace: {
key: "com.example.plugin.stash";
make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>;
makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;
}
StashNamespace
} from './namespace.ts';
import {
class StashedLink
StashedLink
} from './types/StashedLink.ts';
export const
const translations: [{
readonly 'en-US': {
readonly [x: string]: {
readonly 'plugin.name': "Stash";
readonly 'mark-read.label': "Mark as read";
readonly 'typename.label'?: undefined;
readonly 'typename.label_zero'?: undefined;
readonly 'typename.label_one'?: undefined;
readonly 'typename.label_other'?: undefined;
readonly 'object-name.placeholder'?: undefined;
readonly 'add-object.label'?: undefined;
} | {
readonly 'typename.label': "Stashed link";
readonly 'typename.label_zero': "Stashed links";
readonly 'typename.label_one': "Stashed link";
readonly 'typename.label_other': "Stashed links";
readonly 'object-name.placeholder': "New link";
readonly 'add-object.label': "Add link";
readonly 'plugin.name'?: undefined;
readonly 'mark-read.label'?: undefined;
};
readonly "com.example.plugin.stash": {
...;
};
};
}]
translations
= [
{
'en-US': {
[
const StashNamespace: {
key: "com.example.plugin.stash";
make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>;
makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;
}
StashNamespace
.
key: "com.example.plugin.stash"
key
]: {
'plugin.name': 'Stash',
'mark-read.label': 'Mark as read',
},
[
import Type
Type
.
const getTypename: (input: Type.AnyEntity) => string

@returns ―

The typename. Example: com.example.type.person.

Persisted Type.Type entities carry typename in EntityMeta.key (the canonical registry-provenance field); unnamed drafts fall back to the entity's object id so the helper always returns a string. Any dxn: or echo:/ prefix is stripped — typename is a bare identifier, not a URI.

@performance ― O(1) meta read with jsonSchema and id fallbacks.

getTypename
(
class StashedLink
StashedLink
)]: {
'typename.label': 'Stashed link',
'typename.label_zero': 'Stashed links',
'typename.label_one': 'Stashed link',
'typename.label_other': 'Stashed links',
'object-name.placeholder': 'New link',
'add-object.label': 'Add link',
},
},
},
] as
type const = [{
readonly 'en-US': {
readonly [x: string]: {
readonly 'plugin.name': "Stash";
readonly 'mark-read.label': "Mark as read";
} | {
readonly 'typename.label': "Stashed link";
readonly 'typename.label_zero': "Stashed links";
readonly 'typename.label_one': "Stashed link";
readonly 'typename.label_other': "Stashed links";
readonly 'object-name.placeholder': "New link";
readonly 'add-object.label': "Add link";
};
readonly "com.example.plugin.stash": {
readonly 'plugin.name': "Stash";
readonly 'mark-read.label': "Mark as read";
};
};
}]
const
satisfies
import Theme
Theme
.
export Resource
Resource
[];
dx.config.ts
import * as
import AppCapability
AppCapability
from '@dxos/app-toolkit/AppCapability';
16 collapsed lines
import * as
import AppSurface
AppSurface
from '@dxos/app-toolkit/AppSurface';
import * as
import SpaceCapability
SpaceCapability
from '@dxos/plugin-space/SpaceCapability';
import {
const StashNamespace: {
key: "com.example.plugin.stash";
make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>;
makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;
}
StashNamespace
} from './src/namespace.ts';
const
const config: Plugin.PluginConfig<void>
config
:
import Plugin
Plugin
.
type PluginConfig<T = void> = {
readonly plugin: {
readonly key: string;
readonly name: string;
readonly description?: string | undefined;
readonly author?: string | undefined;
readonly homePage?: string | undefined;
readonly source?: string | undefined;
readonly screenshots?: readonly {
readonly light?: string | undefined;
readonly dark?: string | undefined;
}[] | undefined;
readonly tags?: readonly string[] | undefined;
readonly icon?: {
readonly key: string;
readonly hue?: string | undefined;
} | undefined;
readonly spec?: string | undefined;
readonly dependsOn?: readonly string[] | undefined;
};
readonly publish?: {
...;
} | undefined;
} & {
...;
}

A plugin's dx.config.ts default export: its config data, runtime

Meta

and factory.

PluginConfig
=
import Plugin
Plugin
.
const config: <void>({ modules, ...data }: Plugin.ConfigOptions) => Plugin.PluginConfig<void>

Defines a plugin entirely from its dx.config.ts.

@example

// dx.config.ts
export default Plugin.config({
plugin: { key: 'org.dxos.plugin.markdown', name: 'Markdown' },
modules: [AppCapability.schema(() => import('./src/capabilities/schema.ts'))],
});

config
({
plugin: {
readonly key: string;
readonly name: string;
readonly description?: string | undefined;
readonly author?: string | undefined;
readonly homePage?: string | undefined;
readonly source?: string | undefined;
readonly screenshots?: readonly {
readonly light?: string | undefined;
readonly dark?: string | undefined;
}[] | undefined;
readonly tags?: readonly string[] | undefined;
readonly icon?: {
readonly key: string;
readonly hue?: string | undefined;
} | undefined;
readonly spec?: string | undefined;
readonly dependsOn?: readonly string[] | undefined;
}
plugin
: {
key: string

Reverse-domain NSID — the plugin's globally-unique key (e.g. org.dxos.plugin.excalidraw).

key
:
const StashNamespace: {
key: "com.example.plugin.stash";
make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>;
makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;
}
StashNamespace
.
key: "com.example.plugin.stash"
key
,
name: string
name
: 'Stash',
author?: string | undefined

Author or organization name. Only used for bundled plugins. For plugins published to the registry this field is ignored — the verified publisher (handle ?? did) is used instead.

author
: 'Example',
description?: string | undefined
description
:
'A reading list: save links with a note, and mark them as read.',
icon?: {
readonly key: string;
readonly hue?: string | undefined;
} | undefined
icon
: {
key: string
key
: 'ph--tray-arrow-down--regular',
hue?: string | undefined
hue
: 'indigo' },
tags?: readonly string[] | undefined
tags
: ['labs'],
},
modules: readonly (Module<any> | undefined)[]

Module specs only; each body is a dynamic import, so reading the config loads no plugin code.

modules
: [
import AppCapability
AppCapability
.
const translations: (resources: Resource | Resource[] | (() => Promise<{
translations: Resource | Resource[];
}>), options?: {
name?: string;
environments?: readonly Environment[];
}) => Module<void>

Module contributing translations.

translations
(
() => import('./src/translations.ts', {
ImportCallOptions.with?: ImportAttributes | undefined
with
: {
type: string
type
: 'module-url' } }),
),

The app labels objects of a type from that type’s namespace, so a type with these keys reads well in every menu:

Key Where it shows
typename.label The type’s name, such as in the create menu.
typename.label_zero, _one, _other The plural forms, such as a section heading.
object-name.placeholder The name of a new object before the person names it.
add-object.label The create menu’s entry for the type.

useTranslation takes a namespace and returns t. A key with a count picks its plural form.

import * as
import UiHooks
UiHooks
from '@dxos/react-ui/Hooks';
import {
const StashNamespace: {
key: "com.example.plugin.stash";
make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>;
makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;
}
StashNamespace
} from './src/namespace.ts';
export const
const PluginTitle: () => React.JSX.Element
PluginTitle
= () => {
// Strings resolve in the plugin's namespace; the app picks the language.
const {
const t: TFunction<Namespace<string>, KeyPrefix<Namespace<string>>>
t
} =
import UiHooks
UiHooks
.
function useTranslation(ns?: string | string[] | readonly string[] | undefined, options?: UseTranslationOptions<KeyPrefix<Namespace<string>>> | undefined): {
toString(): string;
toLocaleString(): string;
toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string;
pop(): boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n | undefined;
push(...items: (boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n)[]): number;
concat(...items: ConcatArray<boolean | TFunction<...> | import("i18next").i18n>[]): (boolean | TFunction<...> | import("i18next").i18n)[];
concat(...items: (boolean | ConcatArray<boolean | TFunction<...> | import("i18next").i18n> | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[];
join(separator?: string): string;
reverse(): (boolean | TFunction<...> | import("i18next").i18n)[];
shift(): boolean | TFunction<...> | import("i18next").i18n | undefined;
slice(start?: number, end?: number): (boolean | TFunction<...> | import("i18next").i18n)[];
sort(compareFn?: ((a: boolean | TFunction<...> | import("i18next").i18n, b: boolean | TFunction<...> | import("i18next").i18n) => number) | undefined): UseTranslationResponse<...>;
splice(start: number, deleteCount?: number): (boolean | TFunction<...> | import("i18next").i18n)[];
splice(start: number, deleteCount: number, ...items: (boolean | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[];
unshift(...items: (boolean | TFunction<...> | import("i18next").i18n)[]): number;
indexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number;
lastIndexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number;
every<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): this is S[];
every(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean;
some(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean;
forEach(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => void, thisArg?: any): void;
map<U>(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, thisArg?: any): U[];
filter<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): S[];
filter(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): (boolean | TFunction<...> | import("i18next").i18n)[];
reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n;
reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n, initialValue: boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n;
reduce<U>(callbackfn: (previousValue: U, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, initialValue: U): U;
reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n;
reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | ...
export useTranslation
useTranslation
(
const StashNamespace: {
key: "com.example.plugin.stash";
make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>;
makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;
}
StashNamespace
.
key: "com.example.plugin.stash"
key
);
return <
JSX.IntrinsicElements.h2: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>
h2
>{
const t: TFunction
<"plugin.name", ReactI18NextChild | Iterable<ReactI18NextChild>, StringMap>(key: "plugin.name" | "plugin.name"[], options?: string | TOptions<StringMap> | undefined) => ReactI18NextChild | Iterable<ReactI18NextChild> (+1 overload)
t
('plugin.name')}</
JSX.IntrinsicElements.h2: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>
h2
>;
};
import * as
import UiHooks
UiHooks
from '@dxos/react-ui/Hooks';
export const
const LinkCount: ({ count }: {
count: number;
}) => React.JSX.Element
LinkCount
= ({
count: number
count
}: {
count: number
count
: number }) => {
const {
const t: TFunction<Namespace<string>, KeyPrefix<Namespace<string>>>
t
} =
import UiHooks
UiHooks
.
function useTranslation(ns?: string | string[] | readonly string[] | undefined, options?: UseTranslationOptions<KeyPrefix<Namespace<string>>> | undefined): {
toString(): string;
toLocaleString(): string;
toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string;
pop(): boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n | undefined;
push(...items: (boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n)[]): number;
concat(...items: ConcatArray<boolean | TFunction<...> | import("i18next").i18n>[]): (boolean | TFunction<...> | import("i18next").i18n)[];
concat(...items: (boolean | ConcatArray<boolean | TFunction<...> | import("i18next").i18n> | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[];
join(separator?: string): string;
reverse(): (boolean | TFunction<...> | import("i18next").i18n)[];
shift(): boolean | TFunction<...> | import("i18next").i18n | undefined;
slice(start?: number, end?: number): (boolean | TFunction<...> | import("i18next").i18n)[];
sort(compareFn?: ((a: boolean | TFunction<...> | import("i18next").i18n, b: boolean | TFunction<...> | import("i18next").i18n) => number) | undefined): UseTranslationResponse<...>;
splice(start: number, deleteCount?: number): (boolean | TFunction<...> | import("i18next").i18n)[];
splice(start: number, deleteCount: number, ...items: (boolean | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[];
unshift(...items: (boolean | TFunction<...> | import("i18next").i18n)[]): number;
indexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number;
lastIndexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number;
every<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): this is S[];
every(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean;
some(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean;
forEach(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => void, thisArg?: any): void;
map<U>(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, thisArg?: any): U[];
filter<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): S[];
filter(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): (boolean | TFunction<...> | import("i18next").i18n)[];
reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n;
reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n, initialValue: boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n;
reduce<U>(callbackfn: (previousValue: U, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, initialValue: U): U;
reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n;
reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | ...
export useTranslation
useTranslation
('com.example.type.stashedLink');
// `count` picks `typename.label_zero`, `_one` or `_other`.
return <
JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span
>{`${
count: number
count
} ${
const t: TFunction
<"typename.label", string, {
count: number;
}>(key: "typename.label" | "typename.label"[], options?: string | TOptions<{
count: number;
}> | undefined) => string (+1 overload)
t
('typename.label', {
count: number

Count value used for plurals

count
})}`}</
JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span
>;
};

App Graph nodes and actions take a label as a key and namespace, ['mark-read.label', { ns }], and the app translates it where it renders.

The roles a plugin fills most often are in AppSurface from @dxos/app-toolkit:

Role Where it renders What it is given
Article The main content area, one object at a time subject, attendableId
Section An object inside a stack of objects subject, attendableId
CardContent The body of a card, in search results and boards subject
ObjectProperties The object’s properties panel subject
Dialog, Popover A dialog or popover a plugin opens by name component, props
Navigation The navigation sidebar current, the open item’s id
StatusIndicator The status bar no data

A plugin can render into any role another plugin defines, and declare its own. See Surfaces.

Contribute settings, metadata and schema registrations

Section titled “Contribute settings, metadata and schema registrations”

A settings contribution names a prefix, a schema and a writable atom. The app renders a form from the schema and writes the person’s changes to the atom.

import * as
import Effect
Effect
from 'effect/Effect';
import * as
import Atom
Atom
from 'effect/reactivity/Atom';
import * as
import Schema
Schema
from 'effect/Schema';
import * as
import Struct
Struct
from 'effect/Struct';
import * as
import Capability
Capability
from '@dxos/app-framework/Capability';
import * as
import AppCapabilities
AppCapabilities
from '@dxos/app-toolkit/AppCapabilities';
export const
const Settings: Schema.Struct<{
readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;
}>
Settings
=
import Schema
Schema
.
function Struct<{
readonly showSeconds: Schema.optional<Schema.Boolean>;
}>(fields: {
readonly showSeconds: Schema.optional<Schema.Boolean>;
}): Schema.Struct<{
readonly showSeconds: Schema.optional<Schema.Boolean>;
}>

Defines a struct schema from a map of field schemas.

Details

Each field value is a schema. Use

optionalKey

or

optional

to mark fields as optional, and

mutableKey

to mark them as mutable.

The resulting schema's Type is a readonly object type with the fields' decoded types. The Encoded form mirrors the field schemas' encoded types. Declared fields may be inherited and are copied to own properties in the output. The __proto__ field is accepted only when it is an own property. Parsing does not guarantee that output keys retain their input order.

Example (Defining a basic struct)

import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number,
email: Schema.optionalKey(Schema.String)
})
// { readonly name: string; readonly age: number; readonly email?: string }
type Person = typeof Person.Type
Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) // => { name: "Alice", age: 30 }

@stability ― stable

@category ― constructors

@since ― 3.10.0

Struct
({
showSeconds: Schema.optional<Schema.Boolean>
showSeconds
:
import Schema
Schema
.
const optional: optionalLambda
<Schema.Boolean>(self: Schema.Boolean) => Schema.optional<Schema.Boolean>

Type-level representation returned by

optional

.

Marks a struct field as optional, allowing the key to be absent or undefined.

Details

The resulting property may be absent or explicitly set to undefined. Equivalent to optionalKey(UndefinedOr(S)).

Use

optionalKey

instead if you want exact optional semantics (absent only, not undefined).

Example (Defining an optional field accepting undefined)

import { Schema } from "effect"
const schema = Schema.Struct({
name: Schema.String,
age: Schema.optional(Schema.Number)
})
// { readonly name: string; readonly age?: number | undefined }
type Person = typeof schema.Type

@stability ― stable

@category ― models

@since ― 3.10.0

@stability ― stable

@category ― combinators

@since ― 3.10.0

optional
(
import Schema
Schema
.
const Boolean: Schema.Boolean

Type-level representation of

Boolean

.

Schema for boolean values. Validates that the input is typeof "boolean".

When to use

Use to validate values that are already JavaScript booleans.

@stability ― stable

@category ― models

@since ― 4.0.0

@see ― BooleanFromBit for a schema that decodes bit literals 0 or 1 into a boolean

@stability ― stable

@category ― schemas

@since ― 4.0.0

Boolean
.
BottomWithoutNew<boolean, boolean, never, never, Boolean, Boolean, boolean, boolean, readonly [], boolean, "readonly", "required", "no-default", "readonly", "required">.annotate(annotations: Schema.Annotations.Bottom<boolean, readonly []>): Schema.Boolean
annotate
({
Annotations.Augment.title?: string | undefined
title
: 'Show seconds' }),
),
}).
Struct<{ readonly showSeconds: optional<Boolean>; }>.mapFields<{
readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;
}>(f: (fields: {
readonly showSeconds: Schema.optional<Schema.Boolean>;
}) => {
readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;
}, options?: {
readonly unsafePreserveChecks?: boolean | undefined;
} | undefined): Schema.Struct<{
readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;
}>

Returns a new struct with the fields modified by the provided function.

Details

Options:

  • unsafePreserveChecks - if true, keep any .check(...) constraints that were attached to the original union. Defaults to false.

    Warning: This is an unsafe operation. Since mapFields transformations change the schema type, the original refinement functions may no longer be valid or safe to apply to the transformed schema. Only use this option if you have verified that your refinements remain correct after the transformation.

mapFields
(
import Struct
Struct
.
const map: <mutableKeyLambda>(lambda: mutableKeyLambda) => <S>(self: S) => { [K in keyof S]: S[K] extends Schema.Constraint ? Schema.mutableKey<S[K]> : never; } (+1 overload)

Applies a

Lambda

transformation to every value in a struct.

When to use

Use when you want to apply the same function to every value in a struct.

Details

The lambda must be created with

lambda

so the compiler can track the output types.

Example (Wrapping every value in an array)

import { pipe, Struct } from "effect"
interface AsArray extends Struct.Lambda {
<A>(self: A): Array<A>
readonly "~lambda.out": Array<this["~lambda.in"]>
}
const asArray = Struct.lambda<AsArray>((a) => [a])
const result = pipe({ width: 10, height: 20 }, Struct.map(asArray))
result // => { width: [10], height: [20] }

@see ― mapPick – apply a lambda only to selected keys

@see ― mapOmit – apply a lambda to all keys except selected ones

@see ― evolve – apply different functions to different keys

@stability ― stable

@category ― mapping

@since ― 4.0.0

map
(
import Schema
Schema
.
const mutableKey: mutableKeyLambda

Type-level representation returned by

mutableKey

.

Makes a struct field mutable (removes the readonly modifier on the property). Use

readonlyKey

to reverse.

@stability ― stable

@category ― models

@since ― 4.0.0

@stability ― stable

@category ― combinators

@since ― 4.0.0

mutableKey
));
export interface
interface Settings
Settings
extends
import Schema
Schema
.
namespace Schema

Namespace of type-level helpers for

Schema

.

A typed view of a schema that tracks only the decoded (output) type T.

Details

Use Schema<T> as a constraint when you want to accept "any schema that decodes to T" and do not need to know or constrain the encoded representation, required services, or any other type parameters.

This is a structural interface — concrete schema values are produced by the constructors in this module (e.g.

Struct

,

String

,

Number

). When you also need the encoded type or service requirements, use

Codec

.

Example (Accepting any schema decoding to string)

import { Schema } from "effect"
const accept = (_schema: Schema.Schema<string>): void => {}
accept(Schema.String)
accept(Schema.NonEmptyString)

@stability ― stable

@since ― 3.10.0

@see ― Codec — also tracks Encoded, DecodingServices, EncodingServices

@see ― Schema.Type — extract the decoded type at the type level

@stability ― stable

@category ― models

@since ― 3.10.0

Schema
.
type Schema<out T>.Type<S> = S extends {
readonly Type: infer T;
} ? T : never

Extracts the decoded Type from a schema.

Example (Extracting the decoded type)

import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })
type Person = Schema.Schema.Type<typeof Person>
// { readonly name: string; readonly age: number }

@category ― utility types

@since ― 3.10.0

Type
<typeof
const Settings: Schema.Struct<{
readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;
}>
Settings
> {}
export const
const SettingsModule: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, Scope>
SettingsModule
=
import Capability
Capability
.
const makeModule: <void, Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, never>(fn: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, Scope>) => (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, Scope>

Helper to define a capability module with explicit typing. Wraps the default export function to provide better type inference and make the pattern explicit.

This helper provides explicit typing for the module activation function, making it clear that the function should:

  • Access declared requires via yield* (or the ambient Capability.Service/Plugin.Service)
  • Return an array of typed

Contribution

s (see

contribute

/

contributeAll

)

@example

// Module without options - single capability
export default Capability.makeModule(
Effect.fnUntraced(function* () {
const client = yield* ClientCapabilities.Client;
return Capability.contribute(Capabilities.SettingsStore, store);
})
);
// Module with multiple capabilities
export default Capability.makeModule(
Effect.fnUntraced(function* () {
return [
Capability.contribute(Capabilities.SettingsStore, store),
Capability.contribute(Capabilities.Translations, translations),
];
})
);
// Module with required options (context accessed via layer)
export default Capability.makeModule(
Effect.fnUntraced(function* (props: { observability: boolean }) {
const invoker = yield* Capabilities.OperationInvoker;
return Capability.contribute(Capabilities.OperationHandler, ...);
})
);
// Module with scoped resources (closed automatically on deactivation)
export default Capability.makeModule(
Effect.fnUntraced(function* () {
const scope = yield* Scope.Scope;
yield* Scope.addFinalizer(scope, Effect.sync(() => cleanup()));
return Capability.contribute(Capabilities.MyCapability, implementation);
})
);

makeModule
(() =>
import Effect
Effect
.
const sync: <Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>>(thunk: LazyArg<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>>) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, never>

Creates an Effect that represents a synchronous side-effectful computation.

When to use

Use when you need to wrap a synchronous side-effectful operation that is not expected to throw.

Details

The provided function is evaluated lazily when the effect runs.

Gotchas

The function must not throw. If it throws, the thrown value is treated as a defect, not as a typed failure. Use try when throwing is expected.

Example (Capturing synchronous logging in an Effect)

import { Effect } from "effect"
const output: Array<unknown> = []
const log = (message: string) =>
Effect.sync(() => {
void output.push(message) // side effect
})
// ┌─── Effect<void, never, never>
// ▼
const program = log("Hello, World!")
Effect.runSync(program)
output // => ["Hello, World!"]

@see ― try_try for a version that can handle failures.

@stability ― stable

@category ― constructors

@since ― 2.0.0

sync
(() => {
// A writable atom. Real plugins persist it with `KvsStore.make` from
// `@dxos/effect`.
const
const atom: Atom.Writable<Settings, Settings>
atom
=
import Atom
Atom
.
const make: <Settings>(initialValue: Settings) => Atom.Writable<Settings, Settings> (+5 overloads)

Creates an atom from a synchronous value or read function, or from an Effect or Stream whose state is exposed as an AsyncResult; plain values create writable state atoms.

@stability ― unstable

@category ― constructors

@since ― 4.0.0

make
<
interface Settings
Settings
>({
showSeconds?: boolean | undefined
showSeconds
: false });
return
import Capability
Capability
.
const contribute: <Capability.MultiTag<AppCapabilities.Settings, "org.dxos.app-framework.capability.settings">>(capability: Capability.MultiTag<AppCapabilities.Settings, "org.dxos.app-framework.capability.settings">, implementation: AppCapabilities.Settings) => Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">> (+1 overload)
contribute
(
import AppCapabilities
AppCapabilities
.
const Settings: Capability.MultiTag<AppCapabilities.Settings, "org.dxos.app-framework.capability.settings">

@category ― Capability

Settings
, {
prefix: string
prefix
:
const ClockNamespace: {
key: "com.example.plugin.clock";
make: <T>() => <const N extends string>(name: N) => Capability.MultiTag<T, `com.example.plugin.clock.capability.${N}`>;
makeSingleton: <T>() => <const N extends string>(name: N) => Capability.Tag<T, `com.example.plugin.clock.capability.${N}`>;
}
ClockNamespace
.
key: "com.example.plugin.clock"
key
,
schema: Schema.Codec<any, any, never, never>
schema
:
const Settings: Schema.Struct<{
readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;
}>
Settings
,
atom: Atom.Writable<any, any>
atom
,
});
}),
);

In a manifest, each maker takes the loader of the module that contributes the value:

import * as
import AppCapability
AppCapability
from '@dxos/app-toolkit/AppCapability';
// Each maker declares the capability and its default activation event, so a
// manifest names only the loader.
export const
const modules: Capability.Module<void>[]
modules
= [
import AppCapability
AppCapability
.
const settings: <void, void, readonly [], readonly []>(loader: Capability.LoadModule<void, readonly [], readonly [Capability.MultiTag<AppCapabilities.Settings, "org.dxos.app-framework.capability.settings">]>, options?: Capability.MakerOptions<readonly [], readonly [], void, void> | undefined) => Capability.Module<void>

Module maker contributing settings. Ungated by default because settings VALUES are read app-wide through the strict useAtomCapability hook — including from components that mount with the shell at ready (the transcription driver's ReactContext, DeckLayout) and from requires on boot modules — where a post-ready gate trips the missing-capability invariant. A plugin whose settings are read only from its own deferred surfaces declares activatesOn: ActivationEvents.Idle to keep them off the startup pass.

Stated explicitly rather than inherited: omitting activatesOn now normalizes to Idle, which is precisely the post-ready gate this paragraph rules out.

settings
(async () => ({
default: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">> | readonly Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>[], Error, Capability.Requirements<readonly []>>
default
:
const SettingsModule: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, Scope>
SettingsModule
})),
import AppCapability
AppCapability
.
const translations: (resources: Resource | Resource[] | (() => Promise<{
translations: Resource | Resource[];
}>), options?: {
name?: string;
environments?: readonly Capability.Environment[];
}) => Capability.Module<void>

Module contributing translations.

translations
([
{ 'en-US': { [
const ClockNamespace: {
key: "com.example.plugin.clock";
make: <T>() => <const N extends string>(name: N) => Capability.MultiTag<T, `com.example.plugin.clock.capability.${N}`>;
makeSingleton: <T>() => <const N extends string>(name: N) => Capability.Tag<T, `com.example.plugin.clock.capability.${N}`>;
}
ClockNamespace
.
key: "com.example.plugin.clock"
key
]: { 'plugin.name': 'Clock' } } },
]),
];

The tutorial plugin registers its type with AppCapability.schema, whose module’s default export is the list of types:

src/capabilities/schema.ts
import {
class StashedLink
StashedLink
} from '../types/StashedLink.ts';
export default [
class StashedLink
StashedLink
];

Use an app capability when you are adding to something the app already shows: a setting, a node, a skill. Declare a new capability when your plugin is the host and other plugins add to it, the way the Space plugin declares SpaceCapabilities.CreateObjectEntry for the types in its create menu. Capability shows how.

Page What it covers
Capability Declaring and reading capabilities.
Plugin Modules and makers.
React The other hooks a plugin component uses.