Add an operation
By the end of this page the + menu creates links through an operation, and a Mark as read operation marks a link as read.
Background
Section titled “Background”An operation is a named action with a typed input and output. The plugin defines it once and contributes a handler. After that, a button, the command palette and the assistant all run it the same way, through the framework’s operation invoker. Operations explains why.
Creating an object is an operation too. The Space plugin defines SpaceOperation.AddObject, and a plugin uses it to
put a new object in a space.
-
Define an operation. The key is a DXN. The input and output are schemas; an agent reads their descriptions to learn what to pass. The input is a reference to a link rather than the link itself, so the assistant can pass one by its id.
src/types/StashedLinkOperation.ts import * asSchema from 'effect/Schema';import Schemaimport * asOperation from '@dxos/compute/Operation';import Operationimport {Database,import DatabaseDXN,import DXNRef } from '@dxos/echo';import Refimport {StashedLink } from './StashedLink.ts';class StashedLinkexport constMarkRead =const MarkRead: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Database.Service>Operation.import Operationmake({const make: <{readonly meta: {readonly key: DXN.DXN;readonly name: "Mark as read";readonly description: "Mark a link as read.";readonly icon: "ph--check-circle--regular";};readonly services: readonly [typeof Database.Service];readonly input: Schema.Struct<{readonly link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>;}>;readonly output: Schema.Void;}>(props: {readonly meta: {readonly key: DXN.DXN;readonly name: "Mark as read";readonly description: "Mark a link as read.";readonly icon: "ph--check-circle--regular";};readonly services: readonly [typeof Database.Service];readonly input: Schema.Struct<{readonly link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>;}>;readonly output: Schema.Void;}) => Operation.Definition<...>Creates a new Operation definition. Applies default executionMode of 'async' if not specified. The returned type preserves the literal types of props (including services).
meta: {meta: {readonly key: DXN.DXN;readonly name: "Mark as read";readonly description: "Mark a link as read.";readonly icon: "ph--check-circle--regular";}key:key: DXN.DXNDXN.import DXNmake('com.example.operation.stashedLink.markRead'),const make: <"com.example.operation.stashedLink.markRead">(nsid: "com.example.operation.stashedLink.markRead", version?: string) => DXN.DXNConstructs a DXN from an NSID (and optional version). Throws if the result is not a valid DXN. Use
tryMakefor non-throwing string parsing.Static NSID strings are validated at compile time via
Name
: the final segment must be camelCase (no hyphens). Template-literal strings with a runtime prefix are accepted here but still validated at runtime.
name: 'Mark as read',name: "Mark as read"description: 'Mark a link as read.',description: "Mark a link as read."icon: 'ph--check-circle--regular',icon: "ph--check-circle--regular"},// The handler resolves the reference in the link's space.services: [services: readonly [typeof Database.Service]Database.import DatabaseService],class ServiceEffect service tag for Database dependency injection.
input:input: Schema.Struct<{readonly link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>;}>Schema.import SchemaStruct({function Struct<{readonly link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>;}>(fields: {readonly link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>;}): Schema.Struct<{readonly link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>;}>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
Typeis a readonly object type with the fields' decoded types. TheEncodedform 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.TypeSchema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) // => { name: "Alice", age: 30 }link:link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>Ref.import RefRef(const Ref: <typeof StashedLink>(type: typeof StashedLink) => Ref.RefSchema<StashedLink & OfKind<EntityKind.Object> & Unknown> (+3 overloads)StashedLink).class StashedLinkannotate({BottomWithoutNew<unknown, unknown, unknown, unknown, AST, Top, unknown, unknown, any, unknown, Mutability, Optionality, ConstructorDefault, Mutability, Optionality>.annotate(annotations: Schema.Annotations.Bottom<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, any>): Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>description: 'The link that was read.',Annotations.Augment.description?: string | undefined}),}),output:output: Schema.VoidSchema.import SchemaVoid,const Void: Schema.VoidType-level representation of
Void
.
Schema for a TypeScript
voidreturn value.When to use
Use when you need to model the return value of a function, RPC, or endpoint whose result is intentionally ignored.
Details
Runtime parsing accepts any present value and discards it, producing
undefined. The public decoded and encoded TypeScript representation remainsvoid, so typed construction, decoding, and encoding APIs are still modeled asvoid.}); -
Implement the handler.
Operation.withHandlerattaches an Effect to the definition. Operations are built on Effect, andEffect.genreads like an async function, withyield*in place ofawait. This one looks the reference up in the space’s database withDatabase.resolveand sets the link’sreadfield. The operation’sserviceslist what the handler uses from the context, here the space’s database.src/operations/mark-read.ts import * asEffect from 'effect/Effect';import Effectimport * asOperation from '@dxos/compute/Operation';import Operationimport {Database,import DatabaseObj } from '@dxos/echo';import Objimport {StashedLink } from '../types/StashedLink.ts';class StashedLinkimport {MarkRead } from '../types/StashedLinkOperation.ts';const MarkRead: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>consthandler:const handler: Operation.WithHandler<Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>>Operation.import OperationWithHandler<typeoftype WithHandler<T extends Operation.Definition.Any> = T & {handler: Operation.Definition.HandlerType<T>;}MarkRead> =const MarkRead: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>MarkRead.const MarkRead: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>pipe(Pipeable.pipe<Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>, Operation.WithHandler<Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>>>(this: Operation.Definition<...>, ab: (_: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>) => Operation.WithHandler<...>): Operation.WithHandler<...> (+21 overloads)Operation.import OperationwithHandler(({const withHandler: <Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>, EntityNotFoundError>(handler: Operation.Handler<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, EntityNotFoundError, Database.Service | Operation.Service>) => (op: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>) => Operation.WithHandler<...> (+1 overload)link:link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>ref }) =>ref: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>Effect.import Effectgen(function* () {const gen: <Effect.Effect<StashedLink & OfKind<EntityKind.Object>, EntityNotFoundError, Database.Service>, void>(f: () => Generator<Effect.Effect<StashedLink & OfKind<EntityKind.Object>, EntityNotFoundError, Database.Service>, void, never>) => Effect.Effect<void, EntityNotFoundError, Database.Service> (+1 overload)Provides a way to write effectful code using generator functions, simplifying control flow and error handling.
When to use
Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.
Generator functions work similarly to
async/awaitbut keep errors, requirements, and interruption in the Effect type. You canyield*values from effects and return the final result at the end.Example (Sequencing effects with generators)
import { Data, Effect } from "effect"class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}const addServiceCharge = (amount: number) => amount + 1const applyDiscount = (total: number,discountRate: number): Effect.Effect<number, DiscountRateError> =>discountRate === 0? Effect.fail(new DiscountRateError()): Effect.succeed(total - (total * discountRate) / 100)const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))export const program = Effect.gen(function*() {const transactionAmount = yield* fetchTransactionAmountconst discountRate = yield* fetchDiscountRateconst discountedAmount = yield* applyDiscount(transactionAmount,discountRate)const finalAmount = addServiceCharge(discountedAmount)return `Final amount to charge: ${finalAmount}`})await Effect.runPromise(program) // => "Final amount to charge: 96"// The assistant passes a reference by id, so look it up in the space.constlink = yield*const link: StashedLink & OfKind<EntityKind.Object>Database.import Databaseresolve(const resolve: <typeof StashedLink>(ref: URI | Ref<any>, schema: typeof StashedLink) => Effect.Effect<StashedLink & OfKind<EntityKind.Object>, EntityNotFoundError, Database.Service> (+1 overload)ref,ref: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>StashedLink);class StashedLinkObj.import Objupdate(const update: <StashedLink & OfKind<EntityKind.Object>>(obj: StashedLink & OfKind<EntityKind.Object>, callback: ChangeCallback<StashedLink & OfKind<EntityKind.Object>>) => StashedLink & OfKind<EntityKind.Object>Perform mutations on an echo object within a controlled context.
All mutations within the callback are batched and trigger a single notification when the callback completes. Direct mutations outside of
Obj.updatewill throw an error for echo objects.This function also works with nested objects within echo objects (e.g., Template structs) that are reactive at runtime.
link, (const link: StashedLink & OfKind<EntityKind.Object>link) => {link: {title?: string | undefined;url?: string | undefined;note?: string | undefined;read?: boolean | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}link.link: {title?: string | undefined;url?: string | undefined;note?: string | undefined;read?: boolean | undefined;id: EntityId;"~@dxos/echo/Kind": EntityKind.Object;}read = true;read?: boolean | undefined});}),),);export defaulthandler;const handler: Operation.WithHandler<Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;}, void, Database.Service>> -
Contribute the handler. A handler set lists the plugin’s handlers.
Operation.lazyHandlerloads the handler’s file the first time the operation runs, not when the plugin starts.src/operations/index.ts import * asOperation from '@dxos/compute/Operation';import Operationimport * asOperationHandlerSet from '@dxos/compute/OperationHandlerSet';import OperationHandlerSetimport {MarkRead } from '../types/StashedLinkOperation.ts';const MarkRead: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Service>export constStashOperationHandlerSet =const StashOperationHandlerSet: OperationHandlerSet.OperationHandlerSetOperationHandlerSet.import OperationHandlerSetlazy([const lazy: (entries: readonly Operation.LazyHandler[]) => OperationHandlerSet.OperationHandlerSetCreates a handler set from typed
Operation.LazyHandler
pairings: definitions are enumerable without loading any handler body, resolving an operation imports only that operation's module — per-operation loading instead of per-plugin — and each pairing is checked, so a definition cannot be wired to another operation's handler. Loaded handlers are cached per key.
MarkRead.const MarkRead: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Service>pipe(Pipeable.pipe<Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Service>, Operation.LazyHandler<Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Service>>>(this: Operation.Definition<...>, ab: (_: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Service>) => Operation.LazyHandler<...>): Operation.LazyHandler<...> (+21 overloads)Operation.import OperationlazyHandler(() => import('./mark-read.ts'))),const lazyHandler: <Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Service>>(load: () => Promise<{default: Operation.WithHandler<Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Service>>;}>) => (op: Operation.Definition<{readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;}, void, Service>) => Operation.LazyHandler<...> (+1 overload)]);src/capabilities/operation-handler.ts import * asEffect from 'effect/Effect';import Effectimport * asCapabilities from '@dxos/app-framework/Capabilities';import Capabilitiesimport * asCapability from '@dxos/app-framework/Capability';import Capabilityimport {StashOperationHandlerSet } from '../operations/index.ts';const StashOperationHandlerSet: OperationHandlerSetexport defaultCapability.import CapabilitymakeModule(() =>const makeModule: <void, Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.operationHandler", "multi">>, never, never>(fn: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.operationHandler", "multi">>, never, Scope>) => (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.operationHandler", "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
requiresviayield*(or the ambientCapability.Service/Plugin.Service) - Return an array of typed
Contribution
s (see
contribute
/
contributeAll
)
Effect.import Effectsucceed(const succeed: <Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.operationHandler", "multi">>>(value: Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.operationHandler", "multi">>) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.operationHandler", "multi">>, never, never>Creates an
Effectthat always succeeds with a given value.When to use
Use when an effect should complete successfully with a specific value without any errors or external dependencies.
Example (Creating a successful effect)
import { Effect } from "effect"// Creating an effect that represents a successful scenario//// ┌─── Effect<number, never, never>// ▼const success = Effect.succeed(42)Effect.runSync(success) // => 42Capability.import Capabilitycontribute(const contribute: <Capability.MultiTag<OperationHandlerSet, "org.dxos.app-framework.capability.operationHandler">>(capability: Capability.MultiTag<OperationHandlerSet, "org.dxos.app-framework.capability.operationHandler">, implementation: OperationHandlerSet) => Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.operationHandler", "multi">> (+1 overload)Capabilities.import CapabilitiesOperationHandler,const OperationHandler: Capability.MultiTag<OperationHandlerSet, "org.dxos.app-framework.capability.operationHandler">StashOperationHandlerSet,const StashOperationHandlerSet: OperationHandlerSet),),); - Access declared
-
Create a link through an operation. The create-object module adds the type to the + menu and says how to create one: it invokes
SpaceOperation.AddObjectwith a new link frommake. Invoking the operation, rather than adding to the database directly, puts the link wherever the person asked, such as inside a collection.src/capabilities/create-object.ts import * asEffect from 'effect/Effect';import Effectimport * asCapability from '@dxos/app-framework/Capability';import Capabilityimport * asOperation from '@dxos/compute/Operation';import Operationimport {Type } from '@dxos/echo';import Typeimport * asSpaceCapabilities from '@dxos/plugin-space/SpaceCapabilities';import SpaceCapabilitiesimport * asSpaceOperation from '@dxos/plugin-space/SpaceOperation';import SpaceOperationimport {StashedLink,class StashedLinkmake } from '../types/StashedLink.ts';const make: (props?: Partial<MakeProps<typeof StashedLink>>) => StashedLinkexport defaultCapability.import CapabilitymakeModule(() =>const makeModule: <void, Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.plugin.space.capability.createObject", "multi">>, never, never>(fn: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.plugin.space.capability.createObject", "multi">>, never, Scope>) => (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.plugin.space.capability.createObject", "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
requiresviayield*(or the ambientCapability.Service/Plugin.Service) - Return an array of typed
Contribution
s (see
contribute
/
contributeAll
)
Effect.import Effectsucceed(const succeed: <Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.plugin.space.capability.createObject", "multi">>>(value: Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.plugin.space.capability.createObject", "multi">>) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.plugin.space.capability.createObject", "multi">>, never, never>Creates an
Effectthat always succeeds with a given value.When to use
Use when an effect should complete successfully with a specific value without any errors or external dependencies.
Example (Creating a successful effect)
import { Effect } from "effect"// Creating an effect that represents a successful scenario//// ┌─── Effect<number, never, never>// ▼const success = Effect.succeed(42)Effect.runSync(success) // => 42Capability.import Capabilitycontribute(const contribute: <Capability.MultiTag<Readonly<{id: string;createObject: CreateObject;inputSchema?: Codec<any, any>;customPanel?: React.ComponentType<SpaceCapabilities.CreateObjectCustomPanelProps>;}>, "org.dxos.plugin.space.capability.createObject">>(capability: Capability.MultiTag<Readonly<{id: string;createObject: CreateObject;inputSchema?: Codec<any, any>;customPanel?: React.ComponentType<SpaceCapabilities.CreateObjectCustomPanelProps>;}>, "org.dxos.plugin.space.capability.createObject">, implementation: Readonly<...>) => Capability.Contribution<...> (+1 overload)SpaceCapabilities.import SpaceCapabilitiesCreateObjectEntry, {const CreateObjectEntry: Capability.MultiTag<Readonly<{id: string;createObject: CreateObject;inputSchema?: Codec<any, any>;customPanel?: React.ComponentType<SpaceCapabilities.CreateObjectCustomPanelProps>;}>, "org.dxos.plugin.space.capability.createObject">Typed creation entry contributed per typename by plugins that support creating objects.
id:id: stringType.import TypegetTypename(const getTypename: (input: Type.AnyEntity) => stringStashedLink),class StashedLinkcreateObject: (createObject: CreateObjectprops,props: anyoptions) =>options: {db: Database;target?: Unknown;targetNodeId?: string;}Operation.import Operationinvoke(const invoke: <{readonly object: Unknown | {readonly [x: string]: unknown;readonly "@type": string;};readonly target?: import("@dxos/echo/internal").Ref<Unknown> | Unknown | undefined;}, {readonly id: string;readonly object: Unknown;}>(op: Operation.Definition<{readonly object: Unknown | {readonly [x: string]: unknown;readonly "@type": string;};readonly target?: import("@dxos/echo/internal").Ref<Unknown> | Unknown | undefined;}, {readonly id: string;readonly object: Unknown;}, any>, input: {readonly object: Unknown | {readonly [x: string]: unknown;readonly "@type": string;};readonly target?: import("@dxos/echo/internal").Ref<Unknown> | Unknown | undefined;}, options?: Operation.InvokeOptions | undefined) => Effect.Effect<...>Invoke an operation as an Effect. Yields the Operation.Service internally.
SpaceOperation.import SpaceOperationAddObject,const AddObject: Operation.Definition<{readonly object: Unknown | {readonly [x: string]: unknown;readonly "@type": string;};readonly target?: import("@dxos/echo/internal").Ref<Unknown> | Unknown | undefined;}, {readonly id: string;readonly object: Unknown;}, Capability.Service | Service>{object:object: Unknown | {readonly [x: string]: unknown;readonly '@type': string;}make(function make(props?: Partial<MakeProps<typeof StashedLink>>): StashedLinkprops),props: anytarget:target?: Unknown | Ref<Unknown> | undefinedoptions.options: {db: Database;target?: Unknown;targetNodeId?: string;}target },target?: Unknown | undefinedThe created object's parent; absent files at the space root of
db.{spaceId:InvokeOptions.spaceId?: SpaceId | undefinedSpace ID to provide database context for the handler.
options.options: {db: Database;target?: Unknown;targetNodeId?: string;}db.db: DatabasespaceId },Database.spaceId: SpaceId),}),),); - Access declared
-
Add the modules to the manifest.
dx.config.ts import * asAppCapability from '@dxos/app-toolkit/AppCapability';import AppCapability1 collapsed lineimport * asAppSurface from '@dxos/app-toolkit/AppSurface';import AppSurfaceimport * asSpaceCapability from '@dxos/plugin-space/SpaceCapability';import SpaceCapability23 collapsed linesimport {StashNamespace } from './src/namespace.ts';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}`>;}constconfig:const config: Plugin.PluginConfig<void>Plugin.import PluginPluginConfig =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.tsdefault export: its config data, runtimeMeta
and factory.
Plugin.import Pluginconfig({const config: <void>({ modules, ...data }: Plugin.ConfigOptions) => Plugin.PluginConfig<void>Defines a plugin entirely from its
dx.config.ts.plugin: {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;}key:key: stringReverse-domain NSID — the plugin's globally-unique key (e.g.
org.dxos.plugin.excalidraw).StashNamespace.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}`>;}key,key: "com.example.plugin.stash"name: 'Stash',name: stringauthor: 'Example',author?: string | undefinedAuthor 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.
description:description?: string | undefined'A reading list: save links with a note, and mark them as read.',icon: {icon?: {readonly key: string;readonly hue?: string | undefined;} | undefinedkey: 'ph--tray-arrow-down--regular',key: stringhue: 'indigo' },hue?: string | undefinedtags: ['labs'],tags?: readonly string[] | undefined},modules: [modules: readonly (Module<any> | undefined)[]Module specs only; each body is a dynamic import, so reading the config loads no plugin code.
AppCapability.import AppCapabilitytranslations(const translations: (resources: Resource | Resource[] | (() => Promise<{translations: Resource | Resource[];}>), options?: {name?: string;environments?: readonly Environment[];}) => Module<void>Module contributing translations.
() => import('./src/translations.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' } }),type: string),AppCapability.import AppCapabilityschema(const schema: (types: ReadonlyArray<AnyEntity> | (() => Promise<{default: ReadonlyArray<AnyEntity>;}>), options?: {name?: string;environments?: readonly Environment[];}) => Module<void>Module contributing schemas. Prefer the loader form — schema objects ride whatever barrels declare them, so an inline list drags those barrels into the plugin definition's static closure (the boot evaluation floor); a loader keeps them in the module body chunk.
() =>import('./src/capabilities/schema.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' },type: string}),),AppCapability.import AppCapabilityoperationHandler(const operationHandler: <void, any, readonly [], readonly []>(loader: LoadModule<void, readonly [], readonly [MultiTag<OperationHandlerSet, "org.dxos.app-framework.capability.operationHandler">]>, options?: MakerOptions<readonly [], readonly [], void, any> | undefined) => Module<any>Module maker contributing operation handlers. Handler sets register eagerly by default: a keyed set is a definition→loader map with a thin closure (definitions carry no implementations), so the operation registry is complete at boot while each handler's BODY still loads per invocation. The deferral axis is the operation, not the module. A feature plugin whose operations cannot be invoked before the app is interactive declares
activatesOn: ActivationEvents.Idleto register in that wave instead of on the startup pass; the boot path (deck, layout, space, client) invokes operations during startup and stays eager.Stated explicitly rather than inherited: omitting
activatesOnnow normalizes to Idle, which would leave the registry incomplete for exactly those boot-path invocations.() =>import('./src/capabilities/operation-handler.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' },type: string}),),SpaceCapability.import SpaceCapabilitycreateObject(const createObject: <void, any, readonly [], readonly []>(loader: LoadModule<void, readonly [], readonly [MultiTag<Readonly<{id: string;createObject: import("@dxos/plugin-space/SpaceSchema").CreateObject;inputSchema?: import("effect/Schema").Codec<any, any>;customPanel?: import("react").ComponentType<CreateObjectCustomPanelProps>;}>, "org.dxos.plugin.space.capability.createObject">]>, options?: MakerOptions<readonly [], readonly [], void, any> | undefined) => Module<any>Module maker contributing a typed create-object entry. Gated by default on the create flow opening (all consumers read the entries reactively); declare
activatesOnto override.() =>import('./src/capabilities/create-object.ts', {with: {ImportCallOptions.with?: ImportAttributes | undefinedtype: 'module-url' },type: string}),), -
Run it. Load or reload your plugin below. Click New in the header: the inspector runs the create-object module, as Composer’s + menu would, and a link named “New link” becomes the sample object that every module is shown with. Select Operations. The form is built from the operation’s input schema, and its link field takes the sample. Click Run: the operation has no output, so the inspector shows the sample after it ran, with
readset totrue.The links have no view yet. The next page lists them in the navigation tree and puts Mark as read on each link’s toolbar, and the page after that adds a view.
What you can do now
Section titled “What you can do now”- Define an operation with schemas for its input and output, taking an object by reference.
- Implement a handler and load it lazily.
- Create an object of the plugin’s type through the Space plugin’s operation.