Skip to main content

What is a Plugin?

A Plugin defines a custom message type. It controls:
  1. What data a message carries via an input schema (what fields the agent sends)
  2. How that message renders as HTML/CSS/JS in a sandboxed iframe
  3. What logic runs client-side such as HTTP requests, data fetching, user interactions
  4. What configuration it needs like environment variables (API keys, URLs, etc.)
A Plugin does NOT control:
  • Whether a message requires a review/response (that’s the agent’s decision per message)
  • The approve/reject buttons (that’s the review system, orthogonal to plugins)
  • Authentication or routing (that’s the platform)

Plugin vs Review

These are two independent axes on a message:
Message
  ├── metadata.plugin: "form-submit"    ← HOW the message renders (Plugin)
  ├── review: { ... }                   ← WHETHER user input is needed (Review)
  │     ├── type: "approval"            ← WHAT kind of input
  │     └── payload: { options: [...] }
  └── metadata: { name: "John", ... }   ← Plugin-specific data

Plugin Structure

Each plugin lives in packages/plugins/<name>/ and consists of:
  • plugin.json: Manifest with metadata, input schema, env variables, and permissions
  • render.html: HTML + CSS + JS rendered inside a sandboxed iframe
  • icon.svg/.png (optional): Icon shown in the Settings UI
Plugins are discovered automatically on backend startup. No build step required, just add the directory and restart.

Built-in Plugins

Placet ships with two example plugins you can use as reference:
PluginDescriptionSource
Form SubmitRenders a form from structured data and submits it to a configurable webhook URLpackages/plugins/form-submit/
Kroki DiagramRenders diagrams (Mermaid, PlantUML, D2, etc.) via a Kroki serverpackages/plugins/kroki-diagram/

Data Flow

Security Model

ConcernSolution
DOM accesssandbox="allow-scripts" only, no allow-same-origin, no parent DOM access
Cookies/StorageSandboxed iframe has no access to parent cookies or localStorage
HTTP requestsServer-side proxied; domain allowlist enforced via maxHttpDomains
Script injectionPlugin HTML is static per-plugin, loaded from disk
Env valuesStored in DB, injected at render time; secrets not exposed in manifest

Available in Plugins

Inside the iframe, plugins access the Bridge API via the Placet global:
PropertyTypeDescription
Placet.dataRecord<string, unknown>Plugin input data from the message metadata
Placet.envRecord<string, string>Environment variables configured in Settings
Placet.attachmentsAttachmentInfo[]Array of attached files ({ id, filename, mimeType, size })
Placet.messageMessageContextMessage context (id, channelId, senderType, createdAt)
Placet.theme'light' | 'dark'Current theme
Placet.reviewReviewContext | nullReview context ({ type, status, payload }) or null
Placet.isPreviewbooleantrue when rendered in the full-screen preview modal
Methods: Placet.fetch(), Placet.getFile(), Placet.getFileUrl(), Placet.toast(), Placet.respond(), Placet.resize(), Placet.emit(), Placet.on(). See the full Bridge API reference for details.