name: composer-plugin-dev description: Author DXOS Composer plugins — primarily community plugins built in their own repo (Vite + composerPlugin, GitHub release, registered via dxos/community-plugins), with notes on how the in-repo workflow differs. Use when scaffolding a new Composer plugin, wiring capabilities (surfaces, operations, skills), exposing operations to AI agents, integrating external services, testing with the composer testing harness, or publishing to the community registry.
Composer Plugin Authoring
Audience. This skill is for authors building a Composer plugin outside the dxos monorepo. For v1, the recommended layout is an external monorepo containing many community plugins (so refactoring/abstractions can move across them); standalone per-plugin repos like dxos/plugin-excalidraw are also supported and may become the preferred shape in v2. Inside the dxos monorepo (packages/plugins/plugin-*), most patterns are identical, but a handful of things change (build system, dep specifiers, package layout, registration, spec language). Each section below has an "Inside the dxos monorepo" callout where it differs.
Reference plugins.
- Community:
dxos/plugin-excalidraw,dxos/plugin-youtube. - Registry:
dxos/community-plugins— the JSON index Composer reads. - In-repo exemplar:
packages/plugins/plugin-chess.
When in doubt, copy a working plugin and modify.
How to use this skill
Each topic below links to a focused reference file under references/. Read the index first, then jump to the file you need. Files are short and self-contained; don't try to read them all at once.
Read before-you-build.md first. It covers the architectural decisions that should precede any scaffolding: surveying existing plugins, reusing internal types, preferring headless sync layers over bespoke UI, sync-state design, the open question around non-atomic writes, and when to stop and ask a human.
Topics
Before you build
- Before you build — survey existing plugins, reuse internal types, prefer headless sync over bespoke UI, build reusable sync infrastructure, watch out for non-atomic writes, and know when to flag architectural decisions to a human.
Getting started
- Scaffolding a community plugin — minimum viable repo:
package.json,vite.config.ts,tsconfig.json,src/plugin.tsx,src/meta.ts. What to clone fromplugin-excalidraw. - Directory structure — the canonical
src/layout (capabilities/,components/,containers/,types/,operations/,skills/,translations.ts,meta.ts,plugin.tsx). - Plugin definition —
Plugin.define(meta).pipe(...)withAppPlugin.add*Modulehelpers. Activation events. The wiring order that works.
UI
- Components vs containers — the non-negotiable separation.
src/components/are presentational primitives with no@dxos/app-frameworkor@dxos/app-toolkitdependency.src/containers/consume capabilities, are surface-aware, and are always lazy-loaded. Why this split exists and how it keeps your plugin testable. - Components — named exports only, one subdir per component, basic storybook each.
- Containers & UI primitives —
Panel.Root/Panel.Toolbar/Panel.Content/ScrollArea/Toolbar/Cardpatterns. Never write custom layout classNames when a primitive exists. Article/Card/Section role suffix conventions. - React surface —
Surface.create()withAppSurface.object(role, Type)filters. Common roles:article,section,card--content,object-properties,dialog.
Data
- ECHO types & schemas —
Type.makeObject({ typename, version }),LabelAnnotation,Annotation.IconAnnotation, namespace re-export (export * as Foo from './Foo'),make()factory usingObj.make(),FormInputAnnotation. - Translations — keyed by typename (object labels) and
meta.id(plugin-scoped strings).useTranslation(meta.id)in components.
Behavior — keep it out of the UI
- Operations vs UI: where logic belongs — put large computations, side effects, and external-service integrations into operations, not UI code. UI components stay thin. Operations are testable, schema-typed, reusable from skills, callable from CLI, and re-runnable.
- Operations —
Operation.make({ meta, input, output, services }),Operation.withHandler(Effect.fn(...)),OperationHandlerSet.lazy(). Splittingdefinitions.tsfrom per-handler files. Why services declare what they need. - External services & authentication (
AccessToken) — store credentials asAccessTokenECHO objects referenced byRef.Ref<AccessToken>; load inside an operation using an Effect helper. Never read raw secrets in containers. Worked example modeled afterplugin-inbox. - AI inference (
AiService) — when you need an LLM call, do it inside an operation withAiService.model('ai.claude.model.claude-sonnet-4-5'), tools viaOpaqueToolkit, executor viaToolExecutionService. Don't call models from React. - Skills — let agents use your plugin — define a skill key, gather your operations,
Skill.toolDefinitions({ operations }), write a short instruction template. Every plugin worth writing should ship a skill so the assistant can drive it. How to register viaaddSkillDefinitionModule. - Capabilities —
Capability.lazy()incapabilities/index.ts,Capability.makeModule()per file,Capability.contributes(Capabilities.X, ...). Why everything is lazy.
Packaging & publishing
package.json— community-plugin form (single bundled module, deps pinned to a Composer release tag) vs. monorepo form (workspace:*, multipleexports,importsaliases,"private": true). The CLI entrypoint contract:climust export only skill, operations, and types — no React, no app-framework UI capabilities — so it can run under Node.vite.config.ts(community plugins) —composerPlugin({ entry: 'src/plugin.tsx', meta })from@dxos/app-framework/vite-plugin, plusreact()andwasm(). Emitsdist/plugin.mjsanddist/manifest.json.moon.yml(in-repo only) —compile.argslists one--entryPointperpackage.jsonexportssubpath. Skip if you're outside the monorepo.- Publishing to the community registry — full release workflow:
- GitHub Actions build → release with
manifest.json+plugin.mjsassets. - Pin all
@dxos/*deps to the Composer host's main dist-tag. - Local testing via Composer Settings → Plugins → Load by URL.
- Submit a PR to
dxos/community-pluginsadding{ "repo": "owner/repo" }tocommunity-plugins.json. - Registry syncs into Composer roughly every 5 minutes.
- GitHub Actions build → release with
Quality
- Testing with the composer harness —
createComposerTestApp({ plugins: [...] })from@dxos/plugin-testing/harness. Two minimum tests every plugin should have: a smoke test (modules activate on the right events) and an operation test (harness.invoke(MyOp, input)). Use the CLI variant ofClientPlugin(the main one references browser-only capabilities). Storybook for containers. - Coding style —
invariantover throws; barrel imports (#capabilities,#components, etc.); no default exports except containerindex.ts(forReact.lazy); ESM#privateover TSprivatein new code; reactive ECHO (useQuery,useObject, atoms). - Build & verify — community:
pnpm build,pnpm test. Monorepo:moon run plugin-foo:build|lint|test|test-storybook.
Inside the dxos monorepo (only)
PLUGIN.mdlspecification — the design-first spec language (MDL). ThePLUGIN.mdlis the design document. Approve before writing code; update before changing features. Template atpackages/plugins/plugin-spec/docs/PLUGIN-.template.mdl. Community plugins can adopt this voluntarily, but it isn't required.- Registering with
composer-app— for in-repo plugins only: add to the composer app's plugin list. Community plugins are loaded dynamically by Composer from the registry; no registration step.
Quick decision tree
- "Where do I put this code?" Heavy logic →
operations/. External call →operations/(withAccessToken). LLM call →operations/(withAiService). Layout →containers/usingPanel/ScrollArea/Toolbar/Card. Reusable presentational →components/(no framework deps). - "Should agents be able to do this?" If yes — and the answer is usually yes — define an
Operationand reference it from aSkill. - "Where does this belong in
package.jsonexports?" Browser/UI surfaces → main entry. Headless logic the CLI/agents need →./skills,./operations,./types. Keep./clifree of React. - "Do I need
moon.yml?" Only inside the monorepo.
Common mistakes
- Importing
@dxos/app-frameworkfromsrc/components/. (Usecontainers/.) - Calling
fetch/ external APIs / LLMs directly from a container. (Move to an operation.) - Skipping the skill, leaving the assistant unable to drive the plugin.
- Writing custom Tailwind layout instead of using
Panel/ScrollArea/Toolbar/Card. - Adding non-lazy exports to
capabilities/index.ts. - Putting React imports in the CLI entrypoint.
- Hard-coding secrets instead of using
AccessTokenRefs. - Pinning
@dxos/*deps to arbitrary versions instead of the Composer host's main dist-tag.