Building applets
An applet is a self-contained, prebuilt static web app that PilotBPM runs inside a sandboxed iframe. Anyone can build one and submit the repo; the PilotBPM team vets and publishes it. This guide is the full contract.
The security model (read this first)
- Your applet is served from an isolated origin into an iframe with
sandbox="allow-scripts allow-forms allow-popups"— and crucially noallow-same-origin. That means it runs at a null origin: it cannot read PilotBPM cookies, the session, or the parent page. - There is no server runtime. We never run your build, install dependencies, or execute server code. You ship prebuilt static files.
- The only way to touch workspace data is the bridge (below), and every request is re-checked against the scopes an admin approved. Requesting a scope you weren't granted returns an error.
Repository layout
Put a manifest named pilot-applet.json at the repository root, and point it at your built HTML entry:
my-applet/
pilot-applet.json <- manifest (repo root, required)
dist/
index.html <- entry (the "entry" field)
app.js
styles.css
The folder that contains your entry becomes the bundle root. In the example above, dist/ is served as the root, so inside index.html you reference assets relatively (./app.js, ./styles.css). Files outside that folder (e.g. src/) are not shipped.
Manifest: pilot-applet.json
{
"name": "Sticker Sheet",
"version": "1.2.0",
"entry": "dist/index.html",
"description": "Generate printable asset labels.",
"icon": "Boxes",
"category": "Utilities",
"scopes": ["read_current_user", "access_selected_document"],
"csp": ["https://api.example.com"]
}
| Field | Required | Notes |
|---|---|---|
name | yes | Display name. |
entry | yes | Relative path to your HTML entry; must end in .html. No ... |
version | no | Defaults to 1.0.0. |
description, icon, category | no | Used in the marketplace if you don't override them. |
scopes | no | Data scopes you intend to use (see table below). Unknown scopes are rejected at ingest. |
csp | no | Extra origins added to the served page's connect-src (for your own APIs). |
Bundle rules & limits
- Static assets only. Allowed extensions: html, js, mjs, css, json, map, txt, svg, png, jpg, jpeg, gif, webp, ico, bmp, woff, woff2, ttf, otf, eot, wasm.
- Caps: 200 files max, 4 MB per file, 8 MB total.
- The bundle is pinned to an immutable commit SHA at ingest, so what's vetted is exactly what runs. Push an update, then the platform team re-ingests to bump the pinned commit.
The bridge
Your applet talks to PilotBPM with postMessage. The handshake:
- On load, the applet posts
{ pilot: true, type: "ready" }. - The host replies
{ pilot: true, type: "init", scopes, selection }—scopesis the array actually granted,selectionmay contain{ documentId, runId }for the current context. - To fetch data, the applet posts
{ pilot: true, type: "request", id, scope, payload }; the host answers{ pilot: true, type: "response", id, ok, data, error }with the matchingid.
Use the SDK (recommended)
Drop our tiny helper into your bundle and load it before your code:
<script src="applet-sdk.js"></script>
<script>
PilotBPM.ready(function (ctx) {
// ctx.scopes -> granted scopes; ctx.selection -> { documentId?, runId? }
});
async function load() {
const me = await PilotBPM.request("read_current_user");
console.log(me.name, me.email);
}
</script>
PilotBPM.request(scope, payload) returns a Promise that resolves with data or rejects with the host's error. (The SDK is also served at the workspace root path applet-sdk.js.)
Per-user storage
With the applet_storage scope, the SDK exposes a durable key/value store scoped to the current user and your app — perfect for remembering preferences or data like saved signatures. Values are JSON (up to 256 KB per key, 100 keys per app) and sync across the user's devices.
await PilotBPM.storage.set("signatures", [{ name: "Full", dataUrl: "..." }]);
const sigs = await PilotBPM.storage.get("signatures"); // value, or null
const keys = await PilotBPM.storage.keys(); // string[]
await PilotBPM.storage.remove("signatures");
Scopes
Declare what you need in the manifest; an admin approves them on install. Each request is verified server-side against the approved set.
| Scope | Returns |
|---|---|
no_tenant_data | {} — declare this if you need nothing from PilotBPM. |
read_current_user | { id, name, email } of the signed-in user. |
access_selected_document | { id, name, mimeType, sizeBytes, url } for the document in context. |
access_selected_process_run | { id, name, status } for the run in context. |
applet_storage | A durable, per-user, per-app key/value store (isolated per app). |
read_files, write_files | Reserved; acknowledged today, brokered file I/O is on the roadmap. |
Requests are tenant-scoped: you only ever see data from the workspace the applet is running in, and only what the user can access.
Submitting your applet
The preferred path is the managed apps repository — a single curated monorepo PilotBPM maintains. Trust comes from reviewing your pull request before merge, not from auditing a separate repo.
- Add your app under
apps/<slug>/withpilot-applet.jsonand a prebuilt bundle (e.g.apps/<slug>/dist/) committed alongside the source. - Open a pull request. CI builds it and the team reviews the diff.
- Once merged, PilotBPM ingests that folder at a pinned commit. The ingester accepts a subdirectory, so the manifest lives at
apps/<slug>/pilot-applet.jsonand that folder becomes the bundle root.
You can also submit a standalone repo: commit pilot-applet.json at the root, make it reachable, and send the URL to the team (or have a workspace admin use Request an app from their Tools marketplace). Either way the team pins a commit, validates, tests it sandboxed, and publishes it.
Local tips
- Test as a plain static site first (e.g.
npx serve dist). If it works standalone with relative paths, it'll work in the sandbox. - Don't rely on cookies,
localStoragefrom the parent, top-level navigation, or third-party origins you didn't list incsp. - Keep it resilient: if a scope wasn't granted,
requestrejects — handle it and degrade gracefully.