Plugin Development
Build custom tools for Bely using the @usebely/sdk.
Overview
Bely plugins are small React apps that run inside an isolated iframe. They can add new tools to the launcher, search providers, custom actions, and themes. Each plugin is a folder with:
bely.plugin.json— manifest with metadata and permissionsdist/index.js— the built JavaScript bundlesrc/— your source code (included in the bundle for review)
Quick Start
Create a new folder for your plugin and set up the project:
mkdir my-plugin && cd my-plugin pnpm init pnpm add @usebely/sdk react react-dom pnpm add -D esbuild typescript @types/react
Plugin Manifest
Create `bely.plugin.json` at the root of your plugin:
{ "name": "my-plugin", "displayName": "My Plugin", "version": "1.0.0", "description": "What your plugin does", "author": { "name": "Your Name" }, "icon": "package", "category": "dev", "types": { "tool": { "mode": "my-plugin", "aliases": ["myp", "my"] } }, "permissions": ["fetch"], "entry": "dist/index.js", "minBelyVersion": "1.26.0" }
Entry Point
Create `src/index.tsx` — this is your plugin's main file:
import { mount, List, useFetch } from "@usebely/sdk"; import { useState } from "react"; function App() { const [query, setQuery] = useState(""); const { data, loading } = useFetch( query.length >= 2 ? `https://api.example.com/search?q=${query}` : null ); return ( <List searchBarPlaceholder="Search..." onSearchChange={setQuery} isLoading={loading} > {!query.trim() ? ( <EmptyState /> ) : ( data?.results?.map((item) => ( <List.Item key={item.id} id={item.id} title={item.name} subtitle={item.description} /> )) )} </List> ); } function EmptyState() { return ( <div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "30px 20px", color: "var(--glass-text-muted)", }}> <span style={{ fontSize: 12 }}>Type to search</span> </div> ); } mount(<App />);
Build Script
Create `build.mjs` to bundle your plugin with esbuild:
import { build } from "esbuild"; await build({ entryPoints: ["src/index.tsx"], bundle: true, format: "iife", outfile: "dist/index.js", jsx: "automatic", external: ["react", "react-dom"], minify: true, }); console.log("Built dist/index.js");
Add scripts to your `package.json`:
{ "scripts": { "build": "node build.mjs", "dev": "node build.mjs --watch" } }
external in esbuild to exclude them.SDK Components
The SDK provides components that automatically follow the Bely glass theme.
List
Searchable list with items, sections, and action panels.
<List searchBarPlaceholder="Search..." onSearchChange={(text) => {}} isLoading={false} > <List.Item id="1" title="Item name" subtitle="Description" icon="P" accessories={[{ text: "v1.0" }]} actions={ <ActionPanel> <Action title="Open" onAction={() => {}} /> </ActionPanel> } /> <List.Section title="Category"> <List.Item id="2" title="Grouped item" /> </List.Section> </List>
Detail
Detail view with header, markdown, metadata rows, and actions.
<Detail title="Item Name" onBack={() => setSelected(null)} markdown="Description text here" metadata={[ { label: "Version", value: "1.0.0" }, { label: "License", value: "MIT" }, ]} actions={ <ActionPanel> <Action.CopyToClipboard content="npm install pkg" title="Copy" /> <Action.OpenInBrowser url="https://example.com" title="Open" /> </ActionPanel> } />
Form
Form with text fields, text areas, dropdowns, and checkboxes.
<Form onSubmit={(values) => console.log(values)}> <Form.TextField id="name" title="Name" placeholder="Enter name" required /> <Form.TextArea id="desc" title="Description" /> <Form.Dropdown id="type" title="Type" options={[ { label: "Option A", value: "a" }, { label: "Option B", value: "b" }, ]} /> <Form.Checkbox id="active" title="Active" defaultValue={true} /> </Form>
Grid
Grid layout for visual items.
<Grid columns={3} searchBarPlaceholder="Search..."> <Grid.Item id="1" title="Item" subtitle="Info" icon="A" /> </Grid>
Action & ActionPanel
Inline action buttons with copy and open browser shortcuts.
<ActionPanel> <Action title="Run" onAction={() => {}} shortcut="Enter" /> <Action.CopyToClipboard content="text" title="Copy" /> <Action.OpenInBrowser url="https://..." title="Open" /> </ActionPanel>
SDK Hooks
Built-in hooks for common plugin operations.
useFetch
Proxied through Rust (no CORS). Built-in 300ms debounce.
const { data, loading, error, refetch } = useFetch<T>( url, // string | null (null = skip) { debounce: 500 } // optional, default 300ms );
useStorage
Sandboxed per-plugin localStorage.
const { value, setValue, remove } = useStorage<T>("key", defaultValue);
useClipboard
Requires clipboard.read or clipboard.write permission.
const { text, copy, paste } = useClipboard(); await copy("text to copy");
useNavigation
Navigate back within the plugin or exit to the host.
const { pop } = useNavigation(); pop(); // Go back to previous view
usePluginContext
Access plugin ID, query, theme, and locale.
const { pluginId, query, theme, locale } = usePluginContext();
useTranslation
Internationalize your plugin. Automatically uses the host locale (en, pt-BR, es).
const { t, locale } = useTranslation({ en: { greeting: "Hello", search: "Search..." }, "pt-BR": { greeting: "Olá", search: "Buscar..." }, es: { greeting: "Hola", search: "Buscar..." }, }); // t("greeting") → "Olá" (if user locale is pt-BR)
useSystemInfo
Requires system.info permission.
const { os, platform, theme, locale, belyVersion } = useSystemInfo();
Internationalization
Bely supports 3 languages: English (en), Portuguese (pt-BR), and Spanish (es). Use the `useTranslation` hook to localize your plugin automatically based on the user's language.
import { mount, List, useTranslation } from "@usebely/sdk"; const translations = { en: { search: "Search packages...", name: "Name", version: "Version", noResults: "No results found", }, "pt-BR": { search: "Buscar pacotes...", name: "Nome", version: "Versão", noResults: "Nenhum resultado encontrado", }, es: { search: "Buscar paquetes...", name: "Nombre", version: "Versión", noResults: "No se encontraron resultados", }, }; function App() { const { t, locale } = useTranslation(translations); return ( <List searchBarPlaceholder={t("search")}> <List.Item title={t("noResults")} subtitle={locale} /> </List> ); } mount(<App />);
Permissions
Plugins run in a sandboxed iframe. Declare required permissions in the manifest:
"permissions": ["fetch", "clipboard.write"]
storage.get, storage.set) and URL opening (open.url) are always allowed without declaring permissions.Theming
Your plugin automatically inherits the user's theme. The host injects 40+ CSS variables into the iframe.
/* Use CSS variables */ background: var(--glass-btn-bg); border: 1px solid var(--glass-btn-border); color: var(--glass-text); /* Never hardcode colors */ background: #18181B; /* DON'T DO THIS */
Testing Locally
To test your plugin during development:
- Build the plugin:
pnpm build - In Bely, go to Plugin Store > Installed tab
- Click "Install from local folder"
- Select your plugin folder
- The plugin appears in the toolbox
pnpm build
# Then hover the plugin in Installed > click the refresh iconTo update after code changes:
Publishing to the Store
To submit your plugin to the Bely Plugin Store:
- Make sure your plugin has
bely.plugin.json, builtdist/, andsrc/with source code - In Bely, go to Plugin Store > click Submit
- Fill in the metadata (name, description, icon, category, version)
- Select your plugin folder and click Submit for review
- Your plugin will be reviewed and approved or rejected with feedback