Plugin SDK
Plugin adalah paket dengan entry server (definePlugin) dan entry client opsional (definePluginClient). Workspace memasangnya dari store (plugin berbayar dibeli sekali per workspace); plugin bisa menjawab percakapan, memberi alat untuk AI agent, menjual item sendiri, menyajikan halaman publik dan webhook, serta menambah halaman, panel, grup sidebar dan tombol inbox. Kemampuannya dibaca dari definisi: tidak ada daftar kapabilitas yang perlu disamakan.
| Bagian | Fungsinya |
|---|---|
| actions | Satu-satunya pintu masuk: dipanggil dari dashboard, plugin lain, AI (tool) dan API publik (api). Input dicek dengan skemanya; member butuh permission-nya. |
| inbound | Membaca setiap pesan masuk; mengambil percakapan, membalas, menyimpan state, lalu melepasnya. |
| on | Satu event bus: event Beamvia, jadwal (schedules) dan event plugin lain; berjalan di latar. |
| routes | Halaman publik (blok) dan webhook dengan body mentah, di /public/<workspace>/<plugin>/… |
| collections | Storage dengan index, unique key, pencarian teks dan field terenkripsi; claim atomik dan counter. Field di contact ikut terhapus bersama kontaknya. |
| billing | Item yang dijual plugin (kredit, paket); harga, dan lisensi workspace sekali bayar, diatur di store. |
| ai | Instruksi dan konteks live per percakapan untuk agent saat alatnya aktif. |
| ui | Navigasi (grup sidebar sendiri, grup Plugins atau tab pengaturan), halaman dan panel. |
my-plugin/
├── package.json "build": "bun build … (see Publishing)"
├── src/
│ ├── index.ts definePlugin() — runs on the Beamvia server (sandboxed)
│ └── client.tsx definePluginClient() — React pages and dashboard slots (optional)
└── listing/ the public store page (optional, see Store page)
├── listing.json TPluginListing: tagline, description, highlights, FAQ, setup, SEO (en + id)
└── media/ cover (1200×630) and screenshots (1600×1000): PNG, JPEG, WebP or SVGEntry server
definePlugin() menjelaskan semua yang berjalan di server Beamvia, di proses terisolasi. Plugin tidak pernah menyentuh database atau jaringan langsung: ia mendapat ctx yang terbatas pada workspace yang memasangnya.
Hasil inbound
| field | Fungsinya |
|---|---|
| claim | Ambil percakapan; tidak ada yang menjawab sampai kamu melepasnya. |
| release | Kembalikan percakapan ke handler default channel. |
| state | Disimpan di percakapan dan diberikan ke onMessage berikutnya selama kamu memilikinya. |
| replies | Pesan dikirim berurutan (teks, media, lokasi, kontak, kartu PLUGIN). |
| stop | Akhiri pipeline untuk pesan ini tanpa mengambil percakapan (tanpa auto reply, tanpa AI). |
Event
| event | Fungsinya |
|---|---|
| plugin.installed | Dipasang (atau dipasang lagi dalam masa simpan). |
| plugin.upgraded | Workspace pindah ke build lain ({ from, to, isRollback }). |
| plugin.billing.updated | Sebuah item dibeli. |
| inbox.message.created | Pesan di percakapan mana pun (bukan pesanmu sendiri). |
| inbox.conversation.updated | Status, handler atau tag berubah. |
| contact.updated | Kontak berubah. |
| contact.merged | Dua kontak digabung: pindahkan datamu ke primaryId. |
| contact.deleted | Hapus atau anonimkan data kontak itu. |
| schedule.<name> | Cron-mu (schedules) jatuh tempo, di zona waktu workspace. |
| <pluginKey>:<name> | Event dari plugin lain (juga dikirim ke webhook yang berlangganan plugin:<key>:<name>). |
Konteks
Setiap entry point menerima ctx yang terbatas pada workspace pemasang. Metode yang sama berlaku di tes dan di sandbox.
| ctx | Fungsinya |
|---|---|
| ctx.config | Nilai pengaturan (field SECRET didekripsi, hanya di server). |
| ctx.storage | insert, insertMany, find (sort, skip, limit, search), findOne, count, group, update, claim, upsert, remove, next (counter). |
| ctx.inbox | Kirim pesan dan kartu, tulis catatan privat, tambah tag, ubah status, buka percakapan dengan kontak. |
| ctx.contacts | Baca, ubah, cari via telepon atau email, tambah identitas dan memori. |
| ctx.template | Render template dengan variabel percakapan dan datamu sendiri. |
| ctx.plugins | Panggil aksi plugin lain yang terpasang. |
| ctx.events | Kirim "<pluginKey>:<name>" ke plugin dan webhook yang mendengar. |
| ctx.billing | Saldo item; pakai item yang dibeli. |
| ctx.notification | Notifikasi lonceng untuk member dengan permission tertentu. |
| ctx.http | HTTPS ke host di manifest.network (10 dtk, 2 MB, tanpa alamat privat). |
| ctx.files | Simpan file di drive workspace (dihitung ke penyimpanannya), atau baca teks file. url-nya link tetap yang berlaku sampai file-nya dihapus. |
| ctx.email | Email biasa dari alamat platform. |
| ctx.realtime | Update live ke halamanmu yang terbuka (useRealtime). |
| ctx.ui | Jumlah badge di item navigasi. |
| ctx.url | Tautan absolut ke route publik dan halaman dashboard-mu. |
| ctx.ai | AI agent di workspace dan tool Anda mana yang aktif di tiap agent. |
| ctx.logger | Log server yang juga tampil di aktivitas pemasangan. |
import { definePlugin } from "@beamvia/plugin-sdk";
type TConfig = { greeting: string };
export default definePlugin<TConfig>({
manifest: { key: "@acme/greeter", name: "Greeter", description: "Greets new customers", icon: "Hand", version: "1.0.0", author: "Acme", network: ["api.acme.com"] },
config: [{ key: "greeting", label: { en: "Greeting", id: "Sapaan" }, type: "TEXT", default: "Hi <%= contact.name.first %>!" }],
permissions: [{ key: "plugin.greeter.manage", label: "Reset greeter stats" }],
billing: { items: { greetings: { name: { en: "Greeting pack", id: "Paket sapaan" } } } },
// contact: rows are deleted with the contact they belong to
collections: { greeted: { indexes: [["contactId"]], unique: [["contactId"]], contact: ["contactId"] } },
// ** Claim a conversation, answer, then give it back
inbound: {
priority: 50,
onMessage: async (ctx, { contact, isFirstMessage, isOwner }) => {
if (!isFirstMessage || isOwner) return;
await ctx.storage.upsert("greeted", { contactId: contact.id }, { $inc: { count: 1 } });
return { replies: [{ type: "TEXT", text: await ctx.template.render(ctx.config.greeting, { data: {} }) }] };
},
},
// ** The only RPC: dashboard, other plugins, the AI (tool) and the API (api)
actions: {
stats: async (ctx) => ({ count: await ctx.storage.count("greeted") }),
reset: { permission: "plugin.greeter.manage", run: async (ctx) => ({ removed: await ctx.storage.remove("greeted", {}) }) },
greeted_count: {
input: { type: "object", additionalProperties: false, required: [], properties: {} },
tool: { description: "How many customers were greeted", risk: "LOW" },
api: true,
run: async (ctx) => ({ count: await ctx.storage.count("greeted") }),
},
},
// ** One event bus: Beamvia's events, schedules and other plugins' events (background)
on: {
"inbox.message.created": async (ctx, { conversation, message }) => {
if (message.direction === "INBOUND" && /refund/i.test(message.text)) await ctx.inbox.addTag(conversation.id, "refund");
},
"schedule.daily": async (ctx) => ctx.logger.info("Daily tick"),
},
schedules: { daily: "0 9 * * *" },
emits: { greeted: { description: "A customer was greeted" } },
// ** Public pages (blocks) and webhooks: /public/<workspace>/greeter/…
routes: {
"GET hello/:name": async (ctx, request) => ({ page: { title: "Hello", blocks: [{ type: "text", text: `Hi ${request.params.name}` }] } }),
"POST webhook": async (ctx, request) => ({ status: 200, json: { received: request.body.length } }),
},
ai: { instructions: "Greet warmly; never promise refunds.", context: async (ctx, { contact }) => `Greeted before: ${!!(await ctx.storage.findOne("greeted", { contactId: contact.id }))}` },
ui: {
navigation: [{ group: { key: "greeter", label: "Greeter", icon: "Hand" }, items: [{ label: "Stats", path: "stats", icon: "BarChart3" }] }],
pages: [{ path: "stats", title: "Greeter" }], // drawn by the client entry
panels: { contact: async (ctx, { contact }) => [{ type: "text", text: `Contact ${contact.name}` }] },
},
});Entry client
definePluginClient() menambah komponen React dengan JSX biasa. Impor komponen shadcn dashboard dari @beamvia/plugin-sdk/ui dan hook dari @beamvia/plugin-sdk/client; biarkan keduanya, react dan react/jsx-runtime eksternal di bundle agar disediakan dashboard.
| @beamvia/plugin-sdk/client | Fungsinya |
|---|---|
| useQuery | Membaca lewat aksi, di-cache per input. |
| useAction | Menjalankan aksi sekali per klik; menyegarkan query, menampilkan error. |
| usePlugin | Plugin, workspace dan can(permission). |
| useRealtime | Mendengar yang dikirim ctx.realtime.publish. |
| useBilling | Saldo item dan checkout. |
| useNavigate | Pindah ke halaman lain plugin-mu. |
| useCommandCenter | Di dalam jendela atau sidebar Command Center: buka jendela lain milik plugin, atau sebuah chat, sebagai jendela Command Center |
| toast | Toast sukses dan error. |
Slot
| slot | props | Fungsinya |
|---|---|---|
| conversation.panel | conversationId, contactId | Di bawah detail percakapan di inbox. |
| contact.detail | contactId | Di halaman kontak, di samping memori dan identitas. |
| inbox.composer.actions | conversationId, contactId, insertText, send | Tombol di samping composer; insertText menambah ke draf, send mengirim pesan (mis. kartu) atas nama member. |
| inbox.message.actions | conversationId, contactId, message | Item di menu pesan (mis. lampirkan sebagai bukti bayar). |
| inbox.message.render | conversationId, kind, data, direction | Menggambar pesan PLUGIN-mu di inbox; channel menerima fallback-nya. |
| inbox.conversation.banner | conversationId, contactId | Strip di bawah header chat, di samping banner yang dideklarasikan |
import { definePluginClient } from "@beamvia/plugin-sdk";
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@beamvia/plugin-sdk/ui";
import { useAction, useQuery, toast } from "@beamvia/plugin-sdk/client";
const Stats = () => {
const { data, isLoading } = useQuery<{ count: number }>("stats");
const reset = useAction("reset");
if (isLoading) return <Skeleton className="h-24" />;
return (
<Card>
<CardHeader><CardTitle>Greeted customers</CardTitle></CardHeader>
<CardContent className="flex items-center justify-between">
<span className="text-2xl font-semibold">{data?.count}</span>
<Button variant="outline" disabled={reset.isRunning} onClick={() => reset.run().then(() => toast.success("Reset"))}>Reset</Button>
</CardContent>
</Card>
);
};
export default definePluginClient({
key: "@acme/greeter",
pages: { stats: Stats },
slots: {
"inbox.composer.actions": ({ insertText }) => <Button size="sm" variant="ghost" onClick={() => insertText("Hello! 👋")}>Wave</Button>,
},
});Command Center, metrik, dan perintah
Plugin memperluas inbox dan Command Center dengan bagian yang dideklarasikan: Beamvia menggambarnya dengan komponennya sendiri, jadi berjalan di setiap workspace tanpa kode plugin di halaman. Kode client (jendela, sidebar) berjalan di iframe workspace-nya.
| ui.… | Fungsinya |
|---|---|
| metrics | Angka dan badge yang disimpan di kontak dan percakapan (ctx.contacts.metric, ctx.inbox.metric): yang disematkan di baris inbox, semuanya di header chat, kartu hover, dan halaman kontak, serta di konteks AI |
| commands | Item menu yang menjalankan action plugin di baris, pilihan, header chat, halaman kontak, tab Command Center, dan ⌘K |
| banner | Strip di bawah header chat, dengan tombol perintah opsional |
| hover | Bagian (blok) di kartu hover kontak Command Center |
| commandCenter.sources | Daftar data milik plugin, dibaca langsung dari salah satu collection-nya |
| commandCenter.views | Filter tersimpan pada source, tampil sebagai tab; data yang diseret antar tab mengubah field yang bisa ditulis |
| commandCenter.templates | Susunan awal tampilan plugin |
| commandCenter.windows | Jendela yang digambar client (definePluginClient commandCenter.windows), dibuka oleh source, perintah, atau useCommandCenter() |
| commandCenter.status | Hitungan di status bar, diperbarui dengan ctx.ui.status(key, count) |
| Aturan | Fungsinya |
|---|---|
| format | Format metrik: NUMBER, MONEY (dengan mata uang), TEXT, BADGE (dengan tone per nilai), DATE; 20 per plugin per data, string maksimal 200 karakter, setMany menerima 500 |
| target | Target perintah: inbox.row, inbox.selection, chat.header, contact.page, commandCenter.tab, palette. Action menerima { target } dan boleh menjawab { toast }, { open: { window, params } }, atau { navigate } |
| isolation | Plugin hanya melihat workspace tempat ia dipasang; jendela dan sidebar-nya berjalan di iframe workspace itu |
| index | Setiap field yang bisa difilter oleh tampilan, dan item.at, harus ada di salah satu index collection, jika tidak source diabaikan |
| permission | Setiap metrik, perintah, source, jendela, dan item status boleh punya permission (bawaan plugin.use); anggota tanpanya tidak pernah menerimanya |
// ** Metrics on every row, store views, a status count and a row command
ui: {
metrics: {
contact: {
orders: { label: { en: "Orders", id: "Pesanan" }, format: "NUMBER", isPinned: true },
spent: { label: { en: "Spent", id: "Belanja" }, format: "MONEY", currency: "IDR", isPinned: true },
},
},
commands: [{ id: "ship", label: "Mark shipped", target: ["inbox.row", "inbox.selection"], action: "orderShip", when: { metric: "orders" } }],
banner: async (ctx, { contact }) => ({ tone: "warning", text: "Unpaid order #1042", action: { label: "Remind", command: "remind" } }),
commandCenter: {
sources: {
order: {
collection: "orders",
label: "Orders",
item: { title: "number", subtitle: "customer.name", badge: "status", at: "updatedAt" },
fields: [{ key: "status", label: "Status", type: "SELECT", isWritable: true, options: [{ value: "PAID", label: "Paid" }, { value: "SHIPPED", label: "Shipped" }] }],
open: { window: "order" },
},
},
views: [
{ id: "pending", name: "Pending shipment", source: "order", condition: [{ key: "status", op: "is", value: "PAID" }] },
{ id: "shipped", name: "Shipped", source: "order", condition: [{ key: "status", op: "is", value: "SHIPPED" }] },
],
templates: [{ id: "ops", name: "Store ops", layout: { row: [{ view: "pending" }, { view: "shipped" }] } }],
windows: { order: { title: "Order" } },
status: { unpaid: { label: "Unpaid" } },
},
},
collections: { orders: { indexes: [["status", "updatedAt"]] } },
// ** In a listener or schedule
await ctx.contacts.metric.set(contactId, { orders: 12, spent: 1250000 });
await ctx.ui.status("unpaid", 3);
// ** Client: the window the Command Center opens
definePluginClient({ commandCenter: { windows: { order: ({ params }) => <OrderDetail id={params.id} /> } } });Permission
Kunci yang kamu deklarasikan di plugin.<nama>.… (bagian key setelah scope, mis. plugin.store.order.view untuk @acme/store) muncul di editor role di bawah plugin-mu. Permission sebuah aksi menjaganya untuk member; permission halaman dan navigasi menyembunyikannya. Setiap kunci yang dipakai harus dideklarasikan (atau kunci inti seperti plugin.use); upload yang melanggar ditolak. Tambahkan roles: ["AGENT"] pada sebuah kunci agar role sistem itu mendapatkannya saat workspace memasang plugin. Kelas Tailwind di entry client di-build menjadi client.css (tema dashboard) dan dimuat bersamanya.
Navigasi
Navigasi tanpa grup masuk ke grup Plugins bersama; grup SETTING menambah tab pengaturan workspace; objek grup memberi plugin grup sidebar sendiri (mis. Store) dengan badge live.
Menerbitkan plugin
Build kedua entry menjadi bundle CommonJS dengan source map. Saat pengembangan, arahkan PLUGIN_DEV_PATH ke folder plugin-mu: bundle yang di-build ulang langsung dirilis tanpa review. Untuk terbit, upload dist/package.zip (dibuat oleh build) di halaman developer (/developer) atau jalankan bun run publish dengan key developer; Beamvia mereview kodenya lalu kamu merilis build yang disetujui (setelah masa tunggu singkat, bisa bertahap ke sebagian install). Build yang menambah izin atau pengelola baru menunggu persetujuan tiap workspace. Dalam satu versi mayor, jaga storage tetap kompatibel: tambah field, jangan menambah unique key pada field lama atau menghapus kontrak yang disediakan.
# package.json → "build": two CommonJS bundles with source maps
bun build src/index.ts --outdir dist --entry-naming server.js --format cjs --target node --sourcemap=external
bun build src/client.tsx --outdir dist --entry-naming client.js --format cjs --target browser --sourcemap=external \
--external react --external react/jsx-runtime --external react/jsx-dev-runtime \
--external @beamvia/plugin-sdk/ui --external @beamvia/plugin-sdk/client
# Development: Beamvia's PLUGIN_DEV_PATH=/path/to/your/plugins loads every <package>/dist/ bundle,
# lists it in the store and reloads it when you rebuild (no upload, no review; never in production)
# Publishing: upload dist/package.zip on /developer, or bun run publish (with listing/, the store page goes for review too)
# manifest.key must start with your developer scope, e.g. "@acme/greeter"
# manifest.version major.minor.patch, newer than the published one
# → an admin reviews the code (diff against the last version) → approves → publishesHalaman toko
Setiap plugin yang terdaftar punya halaman publik di /plugins/<scope>/<name> yang diindeks mesin pencari dan mesin jawaban AI: tagline, deskripsi (Markdown), keunggulan, galeri, langkah pemasangan, FAQ, harga, ulasan, dan keamanan data. Tulis di tab Store pada halaman developer, atau sertakan di paket sebagai listing/listing.json dengan gambarnya di listing/media (SVG dirender jadi PNG). Url media di sana berupa nama file. Teks bisa satu string atau per bahasa (en, id); terjemahan kosong kembali ke bahasa Inggris. Plugin berbayar wajib punya kebijakan privasi dan email bantuan. Setiap perubahan ditinjau, dan Pratinjau halaman publik menampilkannya sebelum disetujui.
{
"description": {
"short": { "en": "Ask every new customer one quick question", "id": "Tanya satu pertanyaan ke setiap pelanggan baru" },
"long": { "en": "Markdown: what it does, who it is for, how it works", "id": "…" }
},
"highlights": { "en": ["Asks once, on the first message"], "id": ["Bertanya sekali, di pesan pertama"] },
"media": {
"cover": { "url": "cover.svg", "alt": { "en": "Poll: ask every new customer…", "id": "…" } },
"gallery": [{ "type": "IMAGE", "url": "01.svg", "alt": { "en": "Poll asking a new customer…", "id": "…" } }]
},
"faq": [{ "question": { "en": "Can I change the question?", "id": "…" }, "answer": { "en": "Yes…", "id": "…" } }],
"setup": [{ "en": "Install Poll in your workspace.", "id": "…" }],
"categories": ["MARKETING", "ANALYTICS"],
"tags": ["survey", "polling"],
"languages": ["en", "id"],
"support": { "email": "[email protected]", "url": "" },
"links": { "docs": "", "privacy": "https://acme.com/privacy", "terms": "", "website": "https://acme.com", "demo": "" },
"seo": { "title": { "en": "Poll: WhatsApp survey for new customers", "id": "…" }, "description": { "en": "…", "id": "…" }, "keywords": ["whatsapp survey"] }
}
# bun run build packs listing/ into dist/package.zip; bun run publish submits it for review with the build