Hanabi Developer Hub
/ Advanced / API walkthrough
Advanced

API walkthrough

A guided tour of the capability protocol with worked request/response examples.

Module UI
sandboxed iframe
opaque origin
Capability bridge
  1. 1 declared
  2. 2 granted
  3. 3 scoped
  4. 4 REST call
Platform
REST API
Every call is declared, granted and scoped before it touches the platform.

New to coding? This page walks through every single thing a HanabiMatsuri module can ask the platform to do, one call at a time, in plain English. It's a reference: skim the contents, jump to the call you want, copy the tiny example.

You don't need to read it top to bottom. Each entry follows the same little recipe — what the call does, when you'd reach for it, what to pass it, what you get back, the permission it needs, a copy-pasteable snippet, and what the likely error code is if it fails.

  • For the formal tables (the exact message shapes, the security tiers), see capability-api.md — this page links to it instead of repeating it.
  • For what each permission means in everyday terms, see permissions-explained.md.
  • For every error code and how to fix it, see error-codes.md.
  • For runnable sample modules, see examples.md.
  • For the workerAn optional Python program that does heavy or native work off the browser, in a sealed container. (the optional Python half of a module), see worker-guide.md and the H2 at the bottom of this page.

Everything here is mirrored from the real SDK (packages/module-sdk/src/index.ts) and the host that answers it (apps/web/src/lib/module-bridge.ts).


First, how to get the client #

A module is a tiny web page that runs inside a locked box (a sandboxed iframe). It can't touch the platform directly — it can only send a message to the window around it (the "host") and wait for an answer. The client is just the helper that sends those messages and hands you back the reply as a value you can await.

There are two ways to get one. They speak the exact same protocol, so every example below works with either — the only difference is whether you write the message names yourself or get named methods.

Option A — the inline call() bridge client (no build step)

If your module is one plain HTML file, paste this ~12-line helper. It gives you one function, call(type, payload), that sends a request and resolves with the reply. Every demo in examples/modules/ uses it — e.g. demo-files.

js
const PROTOCOL = 1, pending = new Map(); let n = 0;
const call = (type, payload = {}) => new Promise((resolve, reject) => {
  const rid = `r${++n}`; pending.set(rid, { resolve, reject });
  parent.postMessage({ v: PROTOCOL, type, rid, ...payload }, '*');
  setTimeout(() => pending.has(rid) && (pending.delete(rid), reject(new Error('timed out'))), 15000);
});
addEventListener('message', (e) => { const m = e.data; if (!m || !m.rid) return; const s = pending.get(m.rid); if (!s) return; pending.delete(m.rid); m.ok ? s.resolve(m.data) : s.reject(new Error(m.error?.message || 'failed')); });

await call('HANABI_READY');                                  // hello, platform
const { files } = await call('HANABI_PICK_FILE');            // list my input files

With this style you call the raw message name (the HANABI_* string) and pass the payload yourself. Each entry below lists that message name so you can use it.

Option B — createHanabiClient() from @hanabi/module-sdk (named methods)

If your module has a build step, import the SDK. You get a typed client with one named method per callclient.pickFile() instead of call('HANABI_PICK_FILE') — and it handles the request-id plumbing for you.

ts
import { createHanabiClient } from '@hanabi/module-sdk';

const hanabi = createHanabiClient();          // options below
await hanabi.ready();                          // hello, platform
const { files } = await hanabi.pickFile();     // list my input files
// hanabi.dispose();                           // on teardown — see below

Both clients return the same data. Throughout this page, the method name (e.g. pickFile) is Option B; the message (e.g. HANABI_PICK_FILE) is what you'd pass to call() in Option A.

Reading the "If it fails" lines. When a call fails, the reply carries a stable code like HANABI-F002. Look it up in error-codes.md. Three codes can happen to almost any call, so they're not repeated every time: `HANABI-P001` (you didn't list the permission in your manifest), `HANABI-V012` (you passed a missing/wrong-typed argument), and `HANABI-U001` (no one is signed in). The per-call lines below add the codes specific to that call on top of those.

Setup & lifecycle #

createHanabiClient(options?)

  • What it does — builds the client object you call everything else on, and starts listening for the host's replies.
  • When you'd use it — once, at the very top of your module's script, before any other call.
  • Parametersoptions is optional. targetOrigin — who to send messages to (leave it; the default '*' is correct because the sandbox has no real origin). timeoutMs — how long to wait before giving up on a reply (default 15000, i.e. 15 seconds). Streaming calls ignore the timeout.
  • Gives you back — the client: an object with ready, pickFile, writeFile, data, queue, schedules, … and the raw call escape hatch.
  • Permission needed — none (it just creates the helper).
  • Tiny example ``ts import { createHanabiClient } from '@hanabi/module-sdk'; const hanabi = createHanabiClient({ timeoutMs: 30000 }); ``
  • If it fails — it can't; it only sets up the listener. Calls you make with it can fail.

ready() / getContext()

  • What it does — says "I've loaded" to the platform and hands back your module's context: who you are, which permissions you got, your folders, and (if the user opened a file with your module) that file.
  • When you'd use itready() once on startup; getContext() any time you want a fresh copy of that info later.
  • Parameters — none.
  • Gives you back — a context object: moduleId, name, version, permissions (what you asked for), grantedPermissions (what was actually granted), folders (your declared folders, each with id/name/role/path), window, openedFile (the file you were launched with, or null), and launchAction (a Start-menu quick-action id, or null).
  • Permission needed — none.
  • Tiny example ``ts const ctx = await hanabi.ready(); if (ctx.openedFile) console.log('Launched with', ctx.openedFile.name); ``
  • If it failsHANABI-U001 if no session. (Message: HANABI_READY / HANABI_GET_CONTEXT.)

dispose()

  • What it does — stops the client listening for replies. A tidy-up step.
  • When you'd use it — when your view is being torn down (e.g. a single-page app unmounting the component), so you don't leak a listener.
  • Parameters — none.
  • Gives you back — nothing.
  • Permission needed — none.
  • Tiny example ``ts // in your teardown / onDestroy: hanabi.dispose(); ``
  • If it fails — it doesn't (there's no host round-trip).

Files #

Your module owns a few folders (declared in your manifest, e.g. an input folder named Imports and an output folder named Exports). Almost every file call is scopedClamped to your own Modules/<Name>/ folders for the current user — never another module or user. to those folders — you name files by an id string, and the host refuses ids that belong to other modules. The one exception is a file the user explicitly opened with your module (openedFile), which you may read even though it lives elsewhere.

pickFile(accepts?)

  • What it does — lists the files sitting in your module's own input folder.
  • When you'd use it — to show the user the files they've dropped into your Imports folder so they can choose one to process.
  • Parametersaccepts (optional) is a list of file extensions to keep, e.g. ['xlsx', 'csv']. Leave it empty to list everything. (Dots are fine: '.csv'.)
  • Gives you back{ files }, where each file is { id, name, size_bytes, mime_type, extension }. The id is what you pass to the read/write calls.
  • Permission neededfiles.read (see permissions).
  • Tiny example ``js const { files } = await call('HANABI_PICK_FILE', { accepts: ['csv'] }); console.log(files.map((f) => f.name)); ``
  • If it failsHANABI-F002/HANABI-F006 if the module has no readable input folder. (Message: HANABI_PICK_FILE.)

pickUserFile(accepts?)

  • What it does — opens a real file-open dialog over the user's whole workspace and returns the one file they pick (never another module's files).
  • When you'd use it — when your module needs to reach a file outside its own folders, with the user choosing it on the spot.
  • Parametersaccepts (optional), same extension list as pickFile.
  • Gives you back{ file } — a single { id, name, size_bytes, mime_type }, or { file: null } if the user cancelled.
  • Permission needed — the consent-gated files.read.all (the user is prompted the first time).
  • Tiny example ``ts const { file } = await hanabi.pickUserFile(['pdf']); if (file) { /* the user chose one */ } ``
  • If it failsHANABI-P003 if the user hasn't consented to broad access yet. (Message: HANABI_PICK_FILE with scope: 'user'.)

readFile(fileId)

  • What it does — reads a file's bytes and metadata.
  • When you'd use it — once the user has picked a (small-ish) file and you need its actual contents to parse, display, or transform.
  • ParametersfileId — the id string you got back from pickFile, pickUserFile, or openedFile.id.
  • Gives you back{ id, name, mime_type, size_bytes, bytesB64 }. bytesB64 is the file's bytes base64-encoded — decode it (e.g. with atob) to get the raw content.
  • Permission neededfiles.read (or files.read.all for files outside your folders).
  • Tiny example ``js const file = await call('HANABI_READ_FILE', { fileId }); const text = decodeURIComponent(escape(atob(file.bytesB64))); // UTF-8 text ``
  • If it failsHANABI-F001 (no such file), HANABI-F002 (outside your scope), HANABI-F004 (too large — use a worker). (Message: HANABI_READ_FILE.)

openMedia(fileId)

  • What it does — hands back a short-lived streaming URL for a media file, instead of its bytes — drop it straight into a <video>, <audio>, or <img>.
  • When you'd use it — for large or seekable media (a video you want to scrub through) where copying all the bytes would be wasteful. The URL supports seeking, so playback jumps around without moving the whole file.
  • ParametersfileId — the file's id (same scoping rules as readFile).
  • Gives you back{ url, name, mime_type, size_bytes, expires_in }. url is the temporary link; expires_in is how many seconds until it stops working.
  • Permission neededfiles.read (or files.read.all).
  • Tiny example ``ts const media = await hanabi.openMedia(fileId); videoEl.src = media.url; // seekable; no bytes cross the bridge ``
  • If it failsHANABI-F001/HANABI-F002 (missing/out of scope), or HANABI-U004 if the URL expired before you used it. (Message: HANABI_OPEN_MEDIA.)

writeFile(name, bytesB64, folderRole?, subpath?)

  • What it does — saves a new file into one of your module's writable folders.
  • When you'd use it — to deliver a result: a converted image, a generated report, an exported CSV.
  • Parametersname — a plain file name with no slashes (e.g. 'result.txt'). bytesB64 — the file's bytes base64-encoded. folderRole (optional) — which folder to write into by its role; defaults to 'output'. subpath (optional) — a relative folder path to nest the file under that role folder (e.g. 'Trip/Day1'…/Trip/Day1/result.txt); the sub-folders are auto-created (get-or-create, so concurrent writes share them) and the path is sanitized so it can't escape the role folder — handy for preserving an uploaded folder tree without separate fs.makeDir calls.
  • Gives you back{ file: { id, name, size_bytes } } for the file just created.
  • Permission neededfiles.write.
  • Tiny example ``js const utf8ToB64 = (s) => btoa(unescape(encodeURIComponent(s))); // subpath nests the file under an auto-created sub-folder of the role folder: const { file } = await call('HANABI_WRITE_FILE', { name: 'note.txt', bytesB64: utf8ToB64('hello'), folderRole: 'output', subpath: 'Trip/Day1' }); ``
  • If it failsHANABI-F003 (no writable folder declared), HANABI-F005 (unsafe name), HANABI-Q001 (storage full). (Message: HANABI_WRITE_FILE.)

onFilesDropped(cb)

  • What it does — registers a callback for files the user drags onto your module's window, and opts the module in so the host shows a drop target over it.
  • When you'd use it — to accept a dragged file or whole folder (e.g. a batch to process) without making the user click Browse.
  • Why the host forwards them — your module runs in a sandboxed, opaque-origin iframe where the browser withholds dropped directory entries, so a module can't read a dropped folder itself. The host (same-origin) reads the drop and pushes a flat list of { path, file } — each path is relative to the drop, like webkitRelativePath (e.g. Trip/Day1/img.jpg), so a folder-of-folders keeps its structure.
  • Gives you back — an unsubscribe function; call it to stop receiving drops.
  • Permission needed — none (it's a UI opt-in; the user dragging the files is the consent).
  • Tiny example ``js const off = hanabi.onFilesDropped((files) => { for (const { path, file } of files) console.log(path, file.size); }); // later, on teardown: off(); ``
  • If it fails — a drop with no readable files is simply ignored. (Message: HANABI_ENABLE_FILE_DROP.)

readOffice(fileId)

  • What it does — opens a spreadsheet or Word document and returns its structured content (sheets and rows, or paragraphs) — the platform parses it for you, so you don't need a spreadsheet library.
  • When you'd use it — building an in-place editor: load the workbook the user opened your module with, show its cells, let them edit.
  • ParametersfileId — the file's id (the launch file, or one in your own folders).
  • Gives you back — parsed content: { kind: 'spreadsheet', sheets } or { kind: 'document', paragraphs }.
  • Permission neededfiles.read.
  • Tiny example ``ts const wb = await hanabi.readOffice(ctx.openedFile.id); // { kind, sheets } console.log(wb.sheets[0]); ``
  • If it failsHANABI-F001/HANABI-F002 (missing/out of scope). (Message: HANABI_READ_OFFICE.)

saveOffice(fileId, payload)

  • What it does — writes edited spreadsheet/document content back over the same file, in place.
  • When you'd use it — the "Save" button of that in-place editor: the user edited the cells, you save them back to the file they opened.
  • ParametersfileId — the file to overwrite (the launch file, or one in your writable scope). payload — the edited content, shaped as { kind: 'spreadsheet', sheets } or { kind: 'document', paragraphs }.
  • Gives you back — the saved file reference.
  • Permission neededfiles.write.
  • Tiny example ``ts await hanabi.saveOffice(ctx.openedFile.id, { kind: 'spreadsheet', sheets: wb.sheets }); ``
  • If it failsHANABI-V012 if the payload isn't a valid spreadsheet/document shape; HANABI-F002 if the file is out of scope. (Message: HANABI_SAVE_OFFICE.)

openFolder(folderRole?)

  • What it does — opens File Explorer to one of your module's own folders.
  • When you'd use it — a "Show in folder" / "Open Exports" button after you've written output, so the user can find their files.
  • ParametersfolderRole (optional) — which folder to reveal, by role; defaults to 'output'.
  • Gives you back{ path, role } of the folder that was opened.
  • Permission neededfiles.read or files.write.
  • Tiny example ``ts await hanabi.openFolder('output'); // reveal my Exports folder ``
  • If it failsHANABI-F006 if you declared no such folder. (Message: HANABI_OPEN_FOLDER.)

Managing files & folders (hanabi.fs.*) #

writeFile/readFile are enough for "save an output, read an input". When you need to organize — list what's there, make subfolders, rename, move, copy, or delete — use the hanabi.fs.* family. Reads (list, stat) need `files.read`; the rest need `files.manage` (a baseline permission you declare). All of it stays inside your module's own folders. If you also hold the consent-gated files.read.all / files.write.all, the same calls reach the user's whole workspace (never another module's folders). A folder target is your own folder by role ('input'/'output'/'workspace'/…) or any folder by id. Deletes go to the Trash — they're restorable, never a permanent wipe.

fs.list(target?)

  • What it does — lists a folder's files and subfolders.
  • When you'd use it — show the contents of your Exports folder inside the module.
  • Parameterstarget (optional) — { folderRole } or { folderId }; defaults to your input/root folder.
  • Gives you back{ folder, folders, files }.
  • Permission neededfiles.read.
  • Tiny example ``ts const { files } = await hanabi.fs.list({ folderRole: 'output' }); ``

fs.stat(ref)

  • What it does — returns metadata for one file ({ fileId }) or folder ({ folderId }).
  • Gives you back{ file } or { folder }.
  • Permission neededfiles.read.

fs.makeDir(name, parent?)

  • What it does — creates a subfolder.
  • Parametersname; parent (optional) — { parentFolderRole } / { parentFolderId } (defaults to your workspace).
  • Permission neededfiles.manage.
  • Tiny example ``ts const { folder } = await hanabi.fs.makeDir('2026-Q1', { parentFolderRole: 'output' }); ``

fs.rename(ref, name) · fs.move(ref, dest?) · fs.copy(fileId, dest?)

  • What they do — rename a file/folder; move a file/folder into a target folder; copy a file.
  • Parametersref is { fileId } or { folderId }; dest is { destFolderRole } / { destFolderId } (defaults to output). copy takes a fileId.
  • Permission neededfiles.manage (copy also needs files.read to read the source).
  • Tiny example ``ts await hanabi.fs.move({ fileId }, { destFolderRole: 'workspace' }); await hanabi.fs.copy(fileId, { destFolderRole: 'output' }); ``

fs.remove(ref)

  • What it does — moves a file ({ fileId }) or folder ({ folderId }) to the Trash.
  • Permission neededfiles.manage.
  • If it failsHANABI-F002 (out of your scope) or HANABI-P001 (you didn't declare files.manage). (Messages: HANABI_FS_LIST / HANABI_FS_STAT / HANABI_FS_MKDIR / HANABI_FS_RENAME / HANABI_FS_MOVE / HANABI_FS_COPY / HANABI_FS_DELETE.)

Settings & storage #

getSetting(key)

  • What it does — reads back a small saved value you stored under a name.
  • When you'd use it — to remember a user's preference (theme, last tab) across reloads and across their devices.
  • Parameterskey — the name you saved it under (a string).
  • Gives you back — the stored string, or null if you never saved that key. (If you saved an object, you saved its JSON text — parse it back yourself.)
  • Permission neededsettings.self.
  • Tiny example ``js const { value } = await call('HANABI_GET_SETTING', { key: 'theme' }); applyTheme(value ?? 'light'); ` *(With the SDK, getSetting returns the value directly: const theme = await hanabi.getSetting('theme');`.)*
  • If it fails — rarely; falls back to a local copy when the server is unreachable. (Message: HANABI_GET_SETTING.)

setSetting(key, value)

  • What it does — durably saves a small value under a name (per user, per module, server-side — it survives a cache clear and follows the user across devices).
  • When you'd use it — whenever a preference changes and you want it remembered.
  • Parameterskey — the name. value — what to store; a string is stored as-is, anything else is turned into JSON text. Each value is capped at 256 KiB.
  • Gives you back{ key, ok: true }.
  • Permission neededsettings.self.
  • Tiny example ``ts await hanabi.setSetting('theme', 'dark'); ``
  • If it failsHANABI-V012 if key is missing. (Message: HANABI_SET_SETTING.)

storageUsage()

  • What it does — reports how much storage the signed-in user has used and how much they have left.
  • When you'd use it — to show a "you've used X of Y" bar before writing a big output, or to warn near the limit.
  • Parameters — none.
  • Gives you back{ limit_mb, used_bytes, remaining_bytes }. limit_mb and remaining_bytes are null when the user has no limit.
  • Permission neededstorage.read.
  • Tiny example ``ts const s = await hanabi.storageUsage(); console.log(${s.used_bytes} bytes used); ``
  • If it failsHANABI-U001 if not signed in. (Message: HANABI_STORAGE_USAGE.)

Data store (hanabi.data.*) #

A place to keep lists of JSON records that belong to your module and the signed-in user, server-side and cross-device — think an inventory list, a media library, saved projects. Records live in named collections (buckets). Every call here needs the data.store permission. The full runnable example is inventory-tracker.

One thing to know: a stored document's data comes back as a JSON string (the text you saved). Parse it with JSON.parse(doc.data) to get your object.

data.collections()

  • What it does — lists your module's collections with how many records each holds, plus your usage against the quota.
  • When you'd use it — to show a storage/usage summary, or to discover which buckets exist.
  • Parameters — none.
  • Gives you back{ collections: [{ collection, count }], usage: { records, collections, used_chars, limits } }.
  • Permission neededdata.store.
  • Tiny example ``ts const { collections, usage } = await hanabi.data.collections(); console.log(${usage.records} records across ${collections.length} collections); ``
  • If it failsHANABI-P001/HANABI-P002 if data.store isn't granted. (Message: HANABI_DATA_LIST_COLLECTIONS.)

data.list(collection, options?)

  • What it does — returns a page of documents from a collection, newest-updated first.
  • When you'd use it — to show all (or a page of) the records in a bucket, e.g. every inventory item.
  • Parameterscollection — the bucket name. options (optional) — { limit, offset } to page through big lists (limit = how many, offset = how many to skip).
  • Gives you back{ collection, total, limit, offset, docs }, where each doc is { id, collection, data, created_at, updated_at }.
  • Permission neededdata.store.
  • Tiny example ``js const { docs } = await call('HANABI_DATA_LIST', { collection: 'items', limit: 200 }); const items = docs.map((d) => JSON.parse(d.data)); ``
  • If it failsHANABI-V012 if collection is missing. (Message: HANABI_DATA_LIST.)

data.get(collection, id)

  • What it does — fetches one document by its id.
  • When you'd use it — to load a single record you already know the id of (e.g. to open an item's detail view).
  • Parameterscollection — the bucket name. id — the document's id.
  • Gives you back — the document { id, collection, data, created_at, updated_at }, or null if there's no such id.
  • Permission neededdata.store.
  • Tiny example ``ts const doc = await hanabi.data.get('items', someId); if (doc) console.log(JSON.parse(doc.data)); ``
  • If it fails — returns null for a missing id rather than erroring. (Message: HANABI_DATA_GET.)

data.put(collection, data, id?)

  • What it does — saves a document: creates a new one, or overwrites an existing one.
  • When you'd use it — adding a new inventory item, or saving edits to one.
  • Parameterscollection — the bucket name. data — your record (any JSON-serialisable object). id (optional) — omit it to create a new document (the server makes an id); pass it to overwrite that document.
  • Gives you back — the stored document, including its id.
  • Permission neededdata.store.
  • Tiny example ``ts const doc = await hanabi.data.put('items', { name: 'HDMI cable', qty: 3 }); console.log('saved', doc.id); ``
  • If it failsHANABI-Q002 (too many collections), HANABI-Q003 (too many records), HANABI-Q004 (store too large). (Message: HANABI_DATA_PUT.)

data.query(collection, where?, limit?)

  • What it does — finds documents whose top-level fields equal the values you give.
  • When you'd use it — a simple filter: "every item where category is Cables".
  • Parameterscollection — the bucket name. where (optional) — an object of field→value pairs to match, e.g. { category: 'Cables' }. limit (optional) — max results (default 100).
  • Gives you back{ collection, docs, scanned, truncated }. truncated is true when the search hit its scan cap before finishing — page big sets with data.list instead.
  • Permission neededdata.store.
  • Tiny example ``js const { docs } = await call('HANABI_DATA_QUERY', { collection: 'items', where: { category: 'Cables' } }); ``
  • If it failsHANABI-V012 if collection is missing. (Message: HANABI_DATA_QUERY.)

data.delete(collection, id)

  • What it does — removes one document.
  • When you'd use it — a "Delete" button on a record.
  • Parameterscollection — the bucket name. id — the document to remove.
  • Gives you back{ id, collection, existed, ok }. existed is false if it was already gone (not an error).
  • Permission neededdata.store.
  • Tiny example ``ts await hanabi.data.delete('items', someId); ``
  • If it failsHANABI-V012 if id is missing. (Message: HANABI_DATA_DELETE.)

Notifications #

notify(message, options?)

  • What it does — pops a desktop notification (a toast, plus an entry in the notification centre).
  • When you'd use it — to tell the user something finished or needs attention, even if your window isn't focused ("Conversion complete").
  • Parametersmessage — the text (clamped to ~400 chars). options (optional) — { title, tone }. title defaults to your module's name; tone is 'info' (default), 'success', 'warning', or 'error'.
  • Gives you back{ ok: true }.
  • Permission needednotifications.
  • Tiny example ``js await call('HANABI_NOTIFY', { message: 'Export finished', tone: 'success' }); ``
  • If it failsHANABI-V012 if message is missing. (Message: HANABI_NOTIFY.)

setBadge(count)

  • What it does — puts a little number badge on your module's taskbar icon (or clears it).
  • When you'd use it — an unread/pending count: "3 items waiting".
  • Parameterscount — the number to show. `0` or less clears the badge; big numbers render as 99+.
  • Gives you back{ ok: true, count }.
  • Permission needednotifications.
  • Tiny example ``ts await hanabi.setBadge(3); // show "3"; hanabi.setBadge(0) clears it ``
  • If it failsHANABI-P001 if notifications isn't declared. (Message: HANABI_SET_BADGE.)

Jobs (run your module's worker) #

A job runs your module's optional Python workerAn optional Python program that does heavy or native work off the browser, in a sealed container. — the server-side half that does heavy lifting (image processing, generating files). The worker section below and worker-guide.md cover writing one. These two calls are the synchronous path: the call waits for the worker to finish.

createJob(options?, inputRefs?, onProgress?, onOutput?)

  • What it does — runs your worker once and returns the finished job (with the files it produced). Optionally streams live progress and live output while it runs.
  • When you'd use it — the user clicks "Convert" / "Generate": you kick off the worker on the chosen files and show the result (and a progress bar).
  • Parameters
  • options (optional) — a free-form object handed to your worker as its options (e.g. { mode: 'convert', quality: 80 }).
  • inputRefs (optional) — the input files for the worker, as [{ id, name }, …] (ids from pickFile).
  • onProgress (optional) — a callback called repeatedly with { progressPct, message, jobId } as the worker reports progress. Passing it turns on streaming (and removes the timeout).
  • onOutput (optional) — a callback called with { jobId, seq, chunk, encoding } for live output chunks (logs, tokens); decode chunk with atob when encoding is 'base64'.
  • Gives you back{ job }. The job has status, output_refs (the files it wrote, each { id, name }), and progress/error fields.
  • Permission neededjobs.create. (The worker actually running also needs the admin-approved worker.execute — see permissions.)
  • Tiny example ``js const { job } = await call('HANABI_CREATE_JOB', { options: { prefix: 'UPPER' }, inputRefs: [{ id: selected.id, name: selected.name }] }); console.log(job.status, job.output_refs); ` *(Streaming, with the SDK:)* `ts await hanabi.createJob({ mode: 'convert' }, refs, (p) => setProgress(p.progressPct), (o) => appendLog(o.encoding === 'base64' ? atob(o.chunk) : o.chunk)); ``
  • If it failsHANABI-W001 (no worker), HANABI-W003 (worker crashed), HANABI-W004 (timed out), HANABI-Q008 (server busy). (Message: HANABI_CREATE_JOB.)

listJobs()

  • What it does — returns this module's past jobs for the signed-in user (newest first).
  • When you'd use it — an in-module history list, so the user can re-download earlier outputs.
  • Parameters — none.
  • Gives you back — an array of jobs, each { id, status, options, input_refs, output_refs, created_at, completed_at }.
  • Permission neededjobs.create.
  • Tiny example ``ts const jobs = await hanabi.listJobs(); console.log(${jobs.length} past runs); ``
  • If it failsHANABI-P001 if jobs.create isn't declared. (Message: HANABI_LIST_JOBS.)

Background queue (hanabi.queue.*) #

Where createJob waits for the worker, the queue runs it in the background and returns immediately with a "queued" job you can poll later. Use it for long work you don't want to block the window on. Needs the jobs.queue permission. Runnable example: demo-task-queue.

queue.enqueue(options?, inputRefs?)

  • What it does — schedules a worker run in the background and hands back the queued job right away.
  • When you'd use it — "Start export" on a big batch: queue it, let the user keep working, poll for the result.
  • Parametersoptions (optional) and inputRefs (optional) — exactly like createJob's first two arguments.
  • Gives you back — the job (status starts as queued); keep its id to poll.
  • Permission neededjobs.queue.
  • Tiny example ``ts const job = await hanabi.queue.enqueue({ mode: 'convert' }, refs); const jobId = job.id; // poll this with queue.get ``
  • If it failsHANABI-Q005 if you have too many tasks in flight. (Message: HANABI_QUEUE_TASK.)

queue.get(jobId)

  • What it does — checks the current state of one of your queued jobs.
  • When you'd use it — polling, to learn when a background job moved to completed (or failed).
  • ParametersjobId — the id from queue.enqueue.
  • Gives you back — the job, with up-to-date status and (when done) output_refs.
  • Permission neededjobs.queue.
  • Tiny example ``ts const job = await hanabi.queue.get(jobId); if (job.status === 'completed') show(job.output_refs); ``
  • If it failsHANABI-M006 if there's no job with that id for you. (Message: HANABI_GET_JOB.)

queue.cancel(jobId)

  • What it does — cancels a job that is still queued (one already running can't be stopped mid-way).
  • When you'd use it — a "Cancel" button on a pending task.
  • ParametersjobId — the id to cancel.
  • Gives you back — the job in its updated (cancelled) state.
  • Permission neededjobs.queue.
  • Tiny example ``ts await hanabi.queue.cancel(jobId); ``
  • If it failsHANABI-M010 if the job isn't in a cancellable state (e.g. already finished). (Message: HANABI_CANCEL_JOB.)

queue.list()

  • What it does — lists this module's jobs for the user (newest first) — the same history as listJobs, exposed under the queue object.
  • When you'd use it — a task-manager view of queued/running/finished work.
  • Parameters — none.
  • Gives you back — an array of jobs (same shape as listJobs).
  • Permission neededjobs.queue.
  • Tiny example ``ts const jobs = await hanabi.queue.list(); ``
  • If it failsHANABI-P001 if jobs.queue isn't declared. (Message: HANABI_LIST_JOBS.)

Schedules (hanabi.schedules.*) #

A schedule fires a background job automatically on a timer — every N seconds, or on a cron expression — even while your module's window is closed. Use it for backup sync, periodic polling, RSS checks. Needs the admin-approved jobs.schedule permission. Runnable example: demo-scheduler.

schedules.list()

  • What it does — lists this module's schedules for the signed-in user.
  • When you'd use it — to show the user their existing recurring tasks.
  • Parameters — none.
  • Gives you back — an array of schedules, each with id, name, trigger_type, trigger, enabled, next_run_at, last_run_at, and more.
  • Permission neededjobs.schedule.
  • Tiny example ``ts const schedules = await hanabi.schedules.list(); ``
  • If it failsHANABI-P002 if jobs.schedule isn't granted. (Message: HANABI_SCHEDULE_LIST.)

schedules.create(spec)

  • What it does — creates a new recurring schedule.
  • When you'd use it — "Back up every hour": set up the timer once.
  • Parametersspec, an object:
  • name (optional) — a label for the schedule.
  • triggerType (optional) — 'interval' (default) or 'cron'.
  • triggerhow often: { seconds: 3600 } for an interval, or { cron: '0 * * * *' } for cron.
  • options (optional) and inputRefs (optional) — passed to the background job each time it fires, like createJob.
  • Gives you back — the created schedule (with its id).
  • Permission neededjobs.schedule.
  • Tiny example ``ts const s = await hanabi.schedules.create({ name: 'Hourly backup', triggerType: 'interval', trigger: { seconds: 3600 } }); ``
  • If it failsHANABI-Q006 (too many schedules), HANABI-Q007 (interval too short). (Message: HANABI_SCHEDULE_CREATE.)

schedules.toggle(scheduleId, enabled)

  • What it does — turns a schedule on or off (off keeps it, but stops it firing).
  • When you'd use it — a pause/resume switch on a recurring task.
  • ParametersscheduleId — which schedule. enabledtrue to enable, false to pause.
  • Gives you back — the updated schedule.
  • Permission neededjobs.schedule.
  • Tiny example ``ts await hanabi.schedules.toggle(s.id, false); // pause it ``
  • If it failsHANABI-M007 if there's no schedule with that id. (Message: HANABI_SCHEDULE_TOGGLE.)

schedules.delete(scheduleId)

  • What it does — deletes a schedule (and its timer) for good.
  • When you'd use it — the user removes a recurring task.
  • ParametersscheduleId — which schedule to delete.
  • Gives you back{ id, ok }.
  • Permission neededjobs.schedule.
  • Tiny example ``ts await hanabi.schedules.delete(s.id); ``
  • If it failsHANABI-M007 if the id doesn't exist. (Message: HANABI_SCHEDULE_DELETE.)

Talking to other modules #

listModules()

  • What it does — lists the other installed modules the user could hand work to (public facts only — never their data).
  • When you'd use it — to build a "Send to…" menu, or to check whether a partner module is installed.
  • Parameters — none.
  • Gives you back{ modules: [{ id, name, icon, file_associations }] } (your own module is excluded).
  • Permission neededmodules.use.
  • Tiny example ``ts const { modules } = await hanabi.listModules(); ``
  • If it failsHANABI-P001 if modules.use isn't declared. (Message: HANABI_LIST_MODULES.)

sendToModule(moduleId, fileId)

  • What it does — opens another installed module in a new, visible window with one of your files handed to it as its openedFile.
  • When you'd use it — "Open this export in the Image Viewer": pass your file to a sibling module that knows how to display it.
  • ParametersmoduleId — the target module's id (from listModules). fileId — one of your own readable files to hand over.
  • Gives you back{ ok: true, moduleId }.
  • Permission neededmodules.use and files.read.
  • Tiny example ``ts await hanabi.sendToModule('image-viewer', myFileId); ``
  • If it failsHANABI-F002 (the file isn't yours to send), HANABI-M001/ HANABI-M002 (target not installed). (Message: HANABI_SEND_TO_MODULE.)

Desktop workspace (hanabi.desktop.*) #

Your module can integrate with the user's actual Desktop: read it, and drop its own files/folders onto it. Because the Desktop is the user's surface (not your module's folders), these need explicit consent — desktop.read to look, desktop.workspace to place/remove — and the platform only ever lets you remove items you placed.

desktop.list()

  • What it does — returns a snapshot of the user's Desktop: icons (the app shortcuts) and items (files/folders sitting on the Desktop).
  • When you'd use it — check whether you've already dropped your export there.
  • Gives you back{ icons, items }.
  • Permission neededdesktop.read.

desktop.place(ref, { copy? })

  • What it does — puts one of your own files ({ fileId }) or folders ({ folderId }) onto the Desktop so the user sees it there.
  • When you'd use it — "Send to Desktop" after you generate an export.
  • Parametersref is { fileId } or { folderId }; copy: true keeps your original (files only) — the default moves it.
  • Permission neededdesktop.workspace.
  • Tiny example ``ts await hanabi.desktop.place({ fileId: myExportId }, { copy: true }); ``

desktop.remove(ref)

  • What it does — removes from the Desktop a file your module placed there (it goes to the Trash). You can't touch the user's own desktop items.
  • Permission neededdesktop.workspace.
  • If it failsHANABI-P001 (capability not declared), HANABI-F002 (the file isn't yours), HANABI-F001 (you didn't place it). (Messages: HANABI_DESKTOP_LIST / HANABI_DESKTOP_PLACE / HANABI_DESKTOP_REMOVE.)

desktop.createShortcut({ label?, action?, fileId? })

  • What it does — pins a launcher on the Desktop that reopens your module — and optionally deep-links it to a jump-list action and/or one of your files (fileId), which the module receives as its openedFile when the user double-clicks the shortcut.
  • When you'd use it — "Pin this project to the Desktop": the user clicks it and your module reopens straight to that project.
  • Parameterslabel (defaults to your module name), action (a jump-list action id your module handles via launchAction), fileId (one of your own readable files).
  • Gives you back{ shortcut } (with its id).
  • Permission neededdesktop.shortcuts.
  • Tiny example ``ts await hanabi.desktop.createShortcut({ label: 'My Project', fileId: projectFileId }); ``

desktop.removeShortcut(shortcutId)

  • What it does — removes a desktop shortcut your module created (by its id).
  • Permission neededdesktop.shortcuts.
  • If it failsHANABI-P001 (capability not declared) or HANABI-F001 (no such shortcut of yours). (Messages: HANABI_DESKTOP_SHORTCUT / HANABI_DESKTOP_REMOVE_SHORTCUT.)

desktop.getAppearance() · desktop.setAppearance({ wallpaper?, accent?, theme? })

  • What they do — read the workspace appearance ({ wallpaper, accent, theme }), and change it. setAppearance only accepts a built-in wallpaper name (no module-supplied image), an accent as #rrggbb, and a theme of 'light'/'dark'; the platform validates each field and returns the applied appearance (or rejects the change).
  • When you'd use it — a "theme pack" module that switches the workspace look.
  • Permission neededdesktop.personalize.
  • Tiny example ``ts const current = await hanabi.desktop.getAppearance(); await hanabi.desktop.setAppearance({ theme: 'dark', accent: '#5cc8ff' }); ``
  • If it failsHANABI-P001 (capability not declared) or HANABI-V012 (an unknown wallpaper / bad accent / bad theme). (Messages: HANABI_DESKTOP_APPEARANCE / HANABI_DESKTOP_PERSONALIZE.)

Your module's own server routes #

These are only for first-party modules that ship their own server routes (e.g. a /state or /generate endpoint). The host proxies the call to your module's own route subtree and blocks everything else — you can't reach the platform's general routes this way.

moduleRequest(method, path, body?)

  • What it does — calls one of your module's own server routes and returns its JSON response.
  • When you'd use it — when your module has custom server logic (parsing, previewing, saving state) behind its own endpoint.
  • Parametersmethod — the HTTP verb as a string ('GET', 'POST', …). path — your route, relative to your module (e.g. 'state', 'preview'; no leading slash, no ..). body (optional) — a JSON-serialisable request body.
  • Gives you back — whatever your route returns (its parsed JSON).
  • Permission needed — none beyond being a first-party module with a server router (the host clamps the path; see capability-api.md).
  • Tiny example ``ts const preview = await hanabi.moduleRequest('POST', 'preview', { input_refs, options }); ``
  • If it failsHANABI-P004 if the path points outside your module's own routes (or at a reserved route). (Message: HANABI_MODULE_REQUEST.)

moduleStream(method, path, body, onEvent)

  • What it does — the streaming sibling of moduleRequest: consumes one of your routes that emits newline-delimited JSON, calling onEvent for each object as it arrives.
  • When you'd use it — a long route that reports progress line by line (e.g. generating many certificates, one "done" line each).
  • Parametersmethod and path — as moduleRequest. body — the request body. onEvent — a callback called with each emitted object (event) => { … }.
  • Gives you back — a promise that resolves when the stream ends (and rejects if the route emits a terminal { error } line).
  • Permission needed — same as moduleRequest (first-party server route).
  • Tiny example ``ts await hanabi.moduleStream('POST', 'generate-stream', { input_refs, options }, (e) => markDone(e.index, e.total)); ``
  • If it failsHANABI-P004 (path not accessible), or a rejection carrying the route's own error line. (Message: HANABI_MODULE_REQUEST with stream: true.)

The worker half — the Python side #

Most modules are pure UI. A module that needs heavy or native work ships an optional workerAn optional Python program that does heavy or native work off the browser, in a sealed container.: a small Python program the platform runs off the browser, in a locked-down sandbox. The UI starts it with createJob / queue.enqueue (above); this section is the Python contract. Depth lives in worker-guide.md; the simplest runnable worker is demo-worker-basic.

Your worker file (named by entrypoints.worker in the manifest, e.g. worker/main.py) defines one of these two functions.

run(inputs, options, ctx=None) — the small-files contract

  • What it does — receives your input files as bytes in memory, does its work, and returns the output files. The platform writes whatever you return into the module's Exports folder.
  • Parameters
  • inputs — a dict { "filename": b"...bytes..." } of the job's input files. Your worker never sees a path, a file id, the database, or another user.
  • options — the plain dict your UI passed to createJob({...}).
  • ctx (optional) — present only if you declared network.fetch; gives you ctx.fetch(...) for approved network access (below). A 2-argument run(inputs, options) is called unchanged.
  • Gives you back (returns) — the output files, as a dict { "name.ext": b"...bytes..." } (a str is auto-encoded as UTF-8). Lists of ("name", data) tuples or {"name", "data"} dicts also work.
  • Progress — to report progress, make run a generator: yield {"pct": int, "message": str} for each step, then return the outputs. Each yield streams to the UI's onProgress callback (needs jobs.emit_progress).
  • Tiny example ``python # worker/main.py — uppercase every input file def run(inputs, options): prefix = (options or {}).get("prefix", "UPPER") return {f"{prefix}-{name}": data.decode("utf-8", "replace").upper() for name, data in inputs.items()} ``
  • If it failsHANABI-W003 (your code raised), HANABI-W004 (too slow), HANABI-W005 (out of memory), HANABI-W007 (output too large).

run_streaming(in_dir, out_dir, options, ctx=None) — the large-files contract

  • What it does — instead of bytes in memory, the platform streams each input file into a folder (in_dir) and collects whatever files you leave in another folder (out_dir). You read and write by path, so you can handle files of any size with flat memory — ideal for big media (ffmpeg, imagemagick).
  • Parameters
  • in_dir — a folder path holding the input files (read them from here).
  • out_dir — a folder path to write outputs into (the platform collects these).
  • options — the dict from createJob({...}).
  • ctx (optional) — same approved-fetch capability as run.
  • Gives you back (returns) — nothing via return; your outputs are the files you write into `out_dir`. It can yield {"pct", "message"} for progress, just like run.
  • Pick one — a worker defines run or run_streaming (if both exist, run_streaming wins). Use run for small in-memory transforms; run_streaming whenever a tool already reads/writes files.
  • Tiny example ``python def run_streaming(in_dir, out_dir, options, ctx=None): from pathlib import Path src = next(p for p in Path(in_dir).iterdir() if p.is_file()) (Path(out_dir) / f"{src.name}.txt").write_text(f"{src.stat().st_size} bytes\n") ``
  • If it fails — same worker codes as run (HANABI-W003HANABI-W007).

ctx.fetch(url, ...) — approved network from a worker

  • What it does — lets a worker make an HTTP(S) request to an approved origin, brokered by the host (the worker itself has no network socket).
  • When you'd use it — calling an allowed external API (exchange rates, a data feed) from inside a worker.
  • Parametersurl — an absolute https:// URL whose origin you listed in the manifest's dependencies.services. Then method="GET" (default), headers=None, body=None.
  • Gives you back — a dict { "status": int, "headers": {...}, "body": bytes }.
  • Permission needed — the admin-approved network.fetch (or network.fetch.broad for open egress). Without it, ctx.fetch raises a clean "network not approved" error. See permissions.
  • Tiny example ``python def run(inputs, options, ctx): res = ctx.fetch("https://api.example.com/v1/rates", method="GET") return {"rates.json": res["body"]} ``
  • If it failsHANABI-N007/HANABI-N001 (origin not approved), HANABI-N002 (private/loopback blocked), HANABI-N004 (not an http(s) URL), HANABI-N005 (egress budget used up).

The escape hatch: call(type, payload, onProgress?, onOutput?) #

Both clients also expose the raw call. If a new capability lands before this page documents a named method for it, you can always send the message yourself: call takes the HANABI_* message type, a payload object, and (for streaming messages) the same onProgress/onOutput callbacks as createJob. The named methods above are thin wrappers over exactly this.


See also #

  • capability-api.md — the formal capability reference (the message tables, the security-tier table, the SDK method table).
  • error-codes.md — every HANABI-* code, what it means, and how to fix it.
  • examples.md — runnable sample modules, ordered from hello-world to a full worker.
  • permissions-explained.md — what each permission means, in plain language.
  • worker-guide.md — the full worker authoring contract (run / run_streaming, ctx.fetch, resource tiers).
Live: the bridge round-trip click a call — watch it cross the wall
Call an SDK method
Permission granted — toggle to watch a call get denied
Transcript
Pick a call above to run it.