Docs Building applets

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 no allow-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"]
}
FieldRequiredNotes
nameyesDisplay name.
entryyesRelative path to your HTML entry; must end in .html. No ...
versionnoDefaults to 1.0.0.
description, icon, categorynoUsed in the marketplace if you don't override them.
scopesnoData scopes you intend to use (see table below). Unknown scopes are rejected at ingest.
cspnoExtra 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:

  1. On load, the applet posts { pilot: true, type: "ready" }.
  2. The host replies { pilot: true, type: "init", scopes, selection }scopes is the array actually granted, selection may contain { documentId, runId } for the current context.
  3. 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 matching id.

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.

ScopeReturns
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_storageA durable, per-user, per-app key/value store (isolated per app).
read_files, write_filesReserved; 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.

  1. Add your app under apps/<slug>/ with pilot-applet.json and a prebuilt bundle (e.g. apps/<slug>/dist/) committed alongside the source.
  2. Open a pull request. CI builds it and the team reviews the diff.
  3. Once merged, PilotBPM ingests that folder at a pinned commit. The ingester accepts a subdirectory, so the manifest lives at apps/<slug>/pilot-applet.json and 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, localStorage from the parent, top-level navigation, or third-party origins you didn't list in csp.
  • Keep it resilient: if a scope wasn't granted, request rejects — handle it and degrade gracefully.