Skip to content

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.

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.

  1. 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 * as
    import Schema
    Schema
    from 'effect/Schema';
    import * as
    import Operation
    Operation
    from '@dxos/compute/Operation';
    import {
    import Database
    Database
    ,
    import DXN
    DXN
    ,
    import Ref
    Ref
    } from '@dxos/echo';
    import {
    class StashedLink
    StashedLink
    } from './StashedLink.ts';
    export const
    const MarkRead: Operation.Definition<{
    readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;
    }, void, Database.Service>
    MarkRead
    =
    import Operation
    Operation
    .
    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).

    make
    ({
    meta: {
    readonly key: DXN.DXN;
    readonly name: "Mark as read";
    readonly description: "Mark a link as read.";
    readonly icon: "ph--check-circle--regular";
    }
    meta
    : {
    key: DXN.DXN
    key
    :
    import DXN
    DXN
    .
    const make: <"com.example.operation.stashedLink.markRead">(nsid: "com.example.operation.stashedLink.markRead", version?: string) => DXN.DXN

    Constructs a DXN from an NSID (and optional version). Throws if the result is not a valid DXN. Use tryMake for 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.

    @example make('org.dxos.type.calendar') → 'dxn:org.dxos.type.calendar'

    @example make('org.dxos.type.calendar', '1.0.0') → 'dxn:org.dxos.type.calendar:1.0.0'

    make
    ('com.example.operation.stashedLink.markRead'),
    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: readonly [typeof Database.Service]
    services
    : [
    import Database
    Database
    .
    class Service

    Effect service tag for Database dependency injection.

    Service
    ],
    input: Schema.Struct<{
    readonly link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>;
    }>
    input
    :
    import Schema
    Schema
    .
    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 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
    ({
    link: Schema.Codec<Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>, EncodedReference, never, never>
    link
    :
    import Ref
    Ref
    .
    const Ref: <typeof StashedLink>(type: typeof StashedLink) => Ref.RefSchema<StashedLink & OfKind<EntityKind.Object> & Unknown> (+3 overloads)
    Ref
    (
    class StashedLink
    StashedLink
    ).
    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>
    annotate
    ({
    Annotations.Augment.description?: string | undefined
    description
    : 'The link that was read.',
    }),
    }),
    output: Schema.Void
    output
    :
    import Schema
    Schema
    .
    const Void: Schema.Void

    Type-level representation of

    Void

    .

    Schema for a TypeScript void return 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 remains void, so typed construction, decoding, and encoding APIs are still modeled as void.

    @stability ― stable

    @category ― models

    @since ― 3.10.0

    @see ― Undefined for a schema that matches only the exact undefined value.

    @stability ― stable

    @category ― schemas

    @since ― 3.10.0

    Void
    ,
    });
  2. Implement the handler. Operation.withHandler attaches an Effect to the definition. Operations are built on Effect, and Effect.gen reads like an async function, with yield* in place of await. This one looks the reference up in the space’s database with Database.resolve and sets the link’s read field. The operation’s services list what the handler uses from the context, here the space’s database.

    src/operations/mark-read.ts
    import * as
    import Effect
    Effect
    from 'effect/Effect';
    import * as
    import Operation
    Operation
    from '@dxos/compute/Operation';
    import {
    import Database
    Database
    ,
    import Obj
    Obj
    } from '@dxos/echo';
    import {
    class StashedLink
    StashedLink
    } from '../types/StashedLink.ts';
    import {
    const MarkRead: Operation.Definition<{
    readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;
    }, void, Database.Service>
    MarkRead
    } from '../types/StashedLinkOperation.ts';
    const
    const handler: Operation.WithHandler<Operation.Definition<{
    readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;
    }, void, Database.Service>>
    handler
    :
    import Operation
    Operation
    .
    type WithHandler<T extends Operation.Definition.Any> = T & {
    handler: Operation.Definition.HandlerType<T>;
    }
    WithHandler
    <typeof
    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>
    MarkRead
    .
    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)
    pipe
    (
    import Operation
    Operation
    .
    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)
    withHandler
    (({
    link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>
    link
    :
    ref: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>
    ref
    }) =>
    import Effect
    Effect
    .
    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/await but keep errors, requirements, and interruption in the Effect type. You can yield* 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 + 1
    const 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* fetchTransactionAmount
    const discountRate = yield* fetchDiscountRate
    const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
    )
    const finalAmount = addServiceCharge(discountedAmount)
    return `Final amount to charge: ${finalAmount}`
    })
    await Effect.runPromise(program) // => "Final amount to charge: 96"

    @stability ― stable

    @category ― constructors

    @since ― 2.0.0

    gen
    (function* () {
    // The assistant passes a reference by id, so look it up in the space.
    const
    const link: StashedLink & OfKind<EntityKind.Object>
    link
    = yield*
    import Database
    Database
    .
    const resolve: <typeof StashedLink>(ref: URI | Ref<any>, schema: typeof StashedLink) => Effect.Effect<StashedLink & OfKind<EntityKind.Object>, EntityNotFoundError, Database.Service> (+1 overload)
    resolve
    (
    ref: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>
    ref
    ,
    class StashedLink
    StashedLink
    );
    import Obj
    Obj
    .
    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.update will 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.

    @param ― obj - The echo object to mutate. Use Relation.update for relations.

    @param ― callback - The callback that performs mutations on the object.

    @example

    const person = Obj.make(Person, { name: 'John', age: 25 });
    // Mutate within Obj.update
    Obj.update(person, (obj) => {
    obj.name = 'Jane';
    obj.age = 30;
    });
    // ONE notification fires here
    // Direct mutation throws
    person.name = 'Bob'; // Error: Cannot modify outside Obj.update()

    Note: Only accepts objects. Use Relation.update for relations.

    @performance ― Synchronous; costs the mutations made in the callback plus one batched notification and an O(owning fields) parent pass.

    update
    (
    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;
    }
    link
    .
    read?: boolean | undefined
    read
    = true;
    });
    }),
    ),
    );
    export default
    const handler: Operation.WithHandler<Operation.Definition<{
    readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Obj.Unknown>;
    }, void, Database.Service>>
    handler
    ;
  3. Contribute the handler. A handler set lists the plugin’s handlers. Operation.lazyHandler loads the handler’s file the first time the operation runs, not when the plugin starts.

    src/operations/index.ts
    import * as
    import Operation
    Operation
    from '@dxos/compute/Operation';
    import * as
    import OperationHandlerSet
    OperationHandlerSet
    from '@dxos/compute/OperationHandlerSet';
    import {
    const MarkRead: Operation.Definition<{
    readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;
    }, void, Service>
    MarkRead
    } from '../types/StashedLinkOperation.ts';
    export const
    const StashOperationHandlerSet: OperationHandlerSet.OperationHandlerSet
    StashOperationHandlerSet
    =
    import OperationHandlerSet
    OperationHandlerSet
    .
    const lazy: (entries: readonly Operation.LazyHandler[]) => OperationHandlerSet.OperationHandlerSet

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

    @example

    const set = OperationHandlerSet.lazy([
    MarkdownOperation.Create.pipe(Operation.lazyHandler(() => import('./create'))),
    MarkdownOperation.Open.pipe(Operation.lazyHandler(() => import('./open'))),
    ]);

    lazy
    ([
    const MarkRead: Operation.Definition<{
    readonly link: Ref<StashedLink & OfKind<EntityKind.Object> & Unknown>;
    }, void, Service>
    MarkRead
    .
    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)
    pipe
    (
    import Operation
    Operation
    .
    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)
    lazyHandler
    (() => import('./mark-read.ts'))),
    ]);
    src/capabilities/operation-handler.ts
    import * as
    import Effect
    Effect
    from 'effect/Effect';
    import * as
    import Capabilities
    Capabilities
    from '@dxos/app-framework/Capabilities';
    import * as
    import Capability
    Capability
    from '@dxos/app-framework/Capability';
    import {
    const StashOperationHandlerSet: OperationHandlerSet
    StashOperationHandlerSet
    } from '../operations/index.ts';
    export default
    import Capability
    Capability
    .
    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 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 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 Effect that 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) // => 42

    @see ― fail to create an effect that represents a failure.

    @stability ― stable

    @category ― constructors

    @since ― 2.0.0

    succeed
    (
    import Capability
    Capability
    .
    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)
    contribute
    (
    import Capabilities
    Capabilities
    .
    const OperationHandler: Capability.MultiTag<OperationHandlerSet, "org.dxos.app-framework.capability.operationHandler">
    OperationHandler
    ,
    const StashOperationHandlerSet: OperationHandlerSet
    StashOperationHandlerSet
    ,
    ),
    ),
    );
  4. 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.AddObject with a new link from make. 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 * as
    import Effect
    Effect
    from 'effect/Effect';
    import * as
    import Capability
    Capability
    from '@dxos/app-framework/Capability';
    import * as
    import Operation
    Operation
    from '@dxos/compute/Operation';
    import {
    import Type
    Type
    } from '@dxos/echo';
    import * as
    import SpaceCapabilities
    SpaceCapabilities
    from '@dxos/plugin-space/SpaceCapabilities';
    import * as
    import SpaceOperation
    SpaceOperation
    from '@dxos/plugin-space/SpaceOperation';
    import {
    class StashedLink
    StashedLink
    ,
    const make: (props?: Partial<MakeProps<typeof StashedLink>>) => StashedLink
    make
    } from '../types/StashedLink.ts';
    export default
    import Capability
    Capability
    .
    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 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 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 Effect that 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) // => 42

    @see ― fail to create an effect that represents a failure.

    @stability ― stable

    @category ― constructors

    @since ― 2.0.0

    succeed
    (
    import Capability
    Capability
    .
    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)
    contribute
    (
    import SpaceCapabilities
    SpaceCapabilities
    .
    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.

    CreateObjectEntry
    , {
    id: string
    id
    :
    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
    ),
    createObject: CreateObject
    createObject
    : (
    props: any
    props
    ,
    options: {
    db: Database;
    target?: Unknown;
    targetNodeId?: string;
    }
    options
    ) =>
    import Operation
    Operation
    .
    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.

    @example

    yield* Operation.invoke(MyOperation, { data: 'test' });

    invoke
    (
    import SpaceOperation
    SpaceOperation
    .
    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>
    AddObject
    ,
    {
    object: Unknown | {
    readonly [x: string]: unknown;
    readonly '@type': string;
    }
    object
    :
    function make(props?: Partial<MakeProps<typeof StashedLink>>): StashedLink
    make
    (
    props: any
    props
    ),
    target?: Unknown | Ref<Unknown> | undefined
    target
    :
    options: {
    db: Database;
    target?: Unknown;
    targetNodeId?: string;
    }
    options
    .
    target?: Unknown | undefined

    The created object's parent; absent files at the space root of db.

    target
    },
    {
    InvokeOptions.spaceId?: SpaceId | undefined

    Space ID to provide database context for the handler.

    spaceId
    :
    options: {
    db: Database;
    target?: Unknown;
    targetNodeId?: string;
    }
    options
    .
    db: Database
    db
    .
    Database.spaceId: SpaceId
    spaceId
    },
    ),
    }),
    ),
    );
  5. Add the modules to the manifest.

    dx.config.ts
    import * as
    import AppCapability
    AppCapability
    from '@dxos/app-toolkit/AppCapability';
    1 collapsed line
    import * as
    import AppSurface
    AppSurface
    from '@dxos/app-toolkit/AppSurface';
    import * as
    import SpaceCapability
    SpaceCapability
    from '@dxos/plugin-space/SpaceCapability';
    23 collapsed lines
    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' } }),
    ),
    import AppCapability
    AppCapability
    .
    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.

    schema
    (
    () =>
    import('./src/capabilities/schema.ts', {
    ImportCallOptions.with?: ImportAttributes | undefined
    with
    : {
    type: string
    type
    : 'module-url' },
    }),
    ),
    import AppCapability
    AppCapability
    .
    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.Idle to 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 activatesOn now normalizes to Idle, which would leave the registry incomplete for exactly those boot-path invocations.

    operationHandler
    (
    () =>
    import('./src/capabilities/operation-handler.ts', {
    ImportCallOptions.with?: ImportAttributes | undefined
    with
    : {
    type: string
    type
    : 'module-url' },
    }),
    ),
    import SpaceCapability
    SpaceCapability
    .
    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 activatesOn to override.

    createObject
    (
    () =>
    import('./src/capabilities/create-object.ts', {
    ImportCallOptions.with?: ImportAttributes | undefined
    with
    : {
    type: string
    type
    : 'module-url' },
    }),
    ),
  6. 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 read set to true.

    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.

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