Masuk

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.

BagianFungsinya
actionsSatu-satunya pintu masuk: dipanggil dari dashboard, plugin lain, AI (tool) dan API publik (api). Input dicek dengan skemanya; member butuh permission-nya.
inboundMembaca setiap pesan masuk; mengambil percakapan, membalas, menyimpan state, lalu melepasnya.
onSatu event bus: event Beamvia, jadwal (schedules) dan event plugin lain; berjalan di latar.
routesHalaman publik (blok) dan webhook dengan body mentah, di /public/<workspace>/<plugin>/…
collectionsStorage dengan index, unique key, pencarian teks dan field terenkripsi; claim atomik dan counter. Field di contact ikut terhapus bersama kontaknya.
billingItem yang dijual plugin (kredit, paket); harga, dan lisensi workspace sekali bayar, diatur di store.
aiInstruksi dan konteks live per percakapan untuk agent saat alatnya aktif.
uiNavigasi (grup sidebar sendiri, grup Plugins atau tab pengaturan), halaman dan panel.
Layout
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 SVG

Entry 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

fieldFungsinya
claimAmbil percakapan; tidak ada yang menjawab sampai kamu melepasnya.
releaseKembalikan percakapan ke handler default channel.
stateDisimpan di percakapan dan diberikan ke onMessage berikutnya selama kamu memilikinya.
repliesPesan dikirim berurutan (teks, media, lokasi, kontak, kartu PLUGIN).
stopAkhiri pipeline untuk pesan ini tanpa mengambil percakapan (tanpa auto reply, tanpa AI).

Event

eventFungsinya
plugin.installedDipasang (atau dipasang lagi dalam masa simpan).
plugin.upgradedWorkspace pindah ke build lain ({ from, to, isRollback }).
plugin.billing.updatedSebuah item dibeli.
inbox.message.createdPesan di percakapan mana pun (bukan pesanmu sendiri).
inbox.conversation.updatedStatus, handler atau tag berubah.
contact.updatedKontak berubah.
contact.mergedDua kontak digabung: pindahkan datamu ke primaryId.
contact.deletedHapus 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.

ctxFungsinya
ctx.configNilai pengaturan (field SECRET didekripsi, hanya di server).
ctx.storageinsert, insertMany, find (sort, skip, limit, search), findOne, count, group, update, claim, upsert, remove, next (counter).
ctx.inboxKirim pesan dan kartu, tulis catatan privat, tambah tag, ubah status, buka percakapan dengan kontak.
ctx.contactsBaca, ubah, cari via telepon atau email, tambah identitas dan memori.
ctx.templateRender template dengan variabel percakapan dan datamu sendiri.
ctx.pluginsPanggil aksi plugin lain yang terpasang.
ctx.eventsKirim "<pluginKey>:<name>" ke plugin dan webhook yang mendengar.
ctx.billingSaldo item; pakai item yang dibeli.
ctx.notificationNotifikasi lonceng untuk member dengan permission tertentu.
ctx.httpHTTPS ke host di manifest.network (10 dtk, 2 MB, tanpa alamat privat).
ctx.filesSimpan file di drive workspace (dihitung ke penyimpanannya), atau baca teks file. url-nya link tetap yang berlaku sampai file-nya dihapus.
ctx.emailEmail biasa dari alamat platform.
ctx.realtimeUpdate live ke halamanmu yang terbuka (useRealtime).
ctx.uiJumlah badge di item navigasi.
ctx.urlTautan absolut ke route publik dan halaman dashboard-mu.
ctx.aiAI agent di workspace dan tool Anda mana yang aktif di tiap agent.
ctx.loggerLog server yang juga tampil di aktivitas pemasangan.
src/index.ts
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/clientFungsinya
useQueryMembaca lewat aksi, di-cache per input.
useActionMenjalankan aksi sekali per klik; menyegarkan query, menampilkan error.
usePluginPlugin, workspace dan can(permission).
useRealtimeMendengar yang dikirim ctx.realtime.publish.
useBillingSaldo item dan checkout.
useNavigatePindah ke halaman lain plugin-mu.
useCommandCenterDi dalam jendela atau sidebar Command Center: buka jendela lain milik plugin, atau sebuah chat, sebagai jendela Command Center
toastToast sukses dan error.

Slot

slotpropsFungsinya
conversation.panelconversationId, contactIdDi bawah detail percakapan di inbox.
contact.detailcontactIdDi halaman kontak, di samping memori dan identitas.
inbox.composer.actionsconversationId, contactId, insertText, sendTombol di samping composer; insertText menambah ke draf, send mengirim pesan (mis. kartu) atas nama member.
inbox.message.actionsconversationId, contactId, messageItem di menu pesan (mis. lampirkan sebagai bukti bayar).
inbox.message.renderconversationId, kind, data, directionMenggambar pesan PLUGIN-mu di inbox; channel menerima fallback-nya.
inbox.conversation.bannerconversationId, contactIdStrip di bawah header chat, di samping banner yang dideklarasikan
src/client.tsx
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
metricsAngka 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
commandsItem menu yang menjalankan action plugin di baris, pilihan, header chat, halaman kontak, tab Command Center, dan ⌘K
bannerStrip di bawah header chat, dengan tombol perintah opsional
hoverBagian (blok) di kartu hover kontak Command Center
commandCenter.sourcesDaftar data milik plugin, dibaca langsung dari salah satu collection-nya
commandCenter.viewsFilter tersimpan pada source, tampil sebagai tab; data yang diseret antar tab mengubah field yang bisa ditulis
commandCenter.templatesSusunan awal tampilan plugin
commandCenter.windowsJendela yang digambar client (definePluginClient commandCenter.windows), dibuka oleh source, perintah, atau useCommandCenter()
commandCenter.statusHitungan di status bar, diperbarui dengan ctx.ui.status(key, count)
AturanFungsinya
formatFormat 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
targetTarget perintah: inbox.row, inbox.selection, chat.header, contact.page, commandCenter.tab, palette. Action menerima { target } dan boleh menjawab { toast }, { open: { window, params } }, atau { navigate }
isolationPlugin hanya melihat workspace tempat ia dipasang; jendela dan sidebar-nya berjalan di iframe workspace itu
indexSetiap field yang bisa difilter oleh tampilan, dan item.at, harus ada di salah satu index collection, jika tidak source diabaikan
permissionSetiap metrik, perintah, source, jendela, dan item status boleh punya permission (bawaan plugin.use); anggota tanpanya tidak pernah menerimanya
src/server.ts
// ** 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 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.

Build & publish
# 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 → publishes

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

listing/listing.json
{
  "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