Skip to content

Apps (plugins): reference

Manifest (deplao-plugin.json)

Field Required Rules
id yes lowercase reverse-DNS (vn.acme.spa); the last segment (spa) is the node prefix and must not clash with built-in nodes (zalo, fb, trigger…)
name, version yes version follows semver; an update installs only if it is higher than the installed version
apiVersion yes 1
runtime yes web, local (needs backend.entry), remote (needs an https remote.baseUrl)
permissions yes API scopes (see "Authentication & permissions") + the plugin permissions below. May not request admin, plugins:manage, employees:manage, settings:write
ui.entry when there is a UI an .html file inside the plugin; remote: a URL on the same origin as remote.baseUrl
ui.connectSrc no https origins the UI may fetch directly (requires the network permission)
ui.imgSrc no external image sources the UI may display, https://<domain> or https://*.<domain> (e.g. Zalo avatars https://*.zadn.vn; requires the network permission)
minDeplaoVersion no semver
contributes no the extension points below

Plugin permissions

Permission Allows
storage deplao.storage (max 1 MB per value, 20 MB per plugin) and a private data folder
ui:pages pages in the navigation bar
ui:chat-panel a panel on the right of the chat screen that knows which conversation is open
ui:composer buttons in the message composer, deplao.ui.insertComposerText
ui:microphone the microphone inside the app's UI (the frame gets allow="microphone"; without this permission the browser blocks getUserMedia)
workflow:nodes workflow nodes and triggers, deplao.workflow.trigger
events:emit deplao.events.emit (names declared in contributes.events)
network deplao.http.fetch in the backend, ui.connectSrc
schedules contributes.schedules (cron)
cloudflare:workers deplao.cloudflare in the backend (runtime local): create / update Workers and D1 databases named <prefix> or <prefix>-… (prefix = the last segment of the id) on the Cloudflare account connected in Deplao; the Cloudflare token is never handed to the app

Extension points (contributes)

Point Declaration Notes
pages { id, title, path } path is a hash route inside ui.entry
chatPanels { id, title, path, width } receives deplao.ui.context() and onContext
composerButtons { id, title, icon, target: 'ui' | 'backend' } backend: commands[id](context) returns { insertText } or { toast }
settings.schema JSON Schema (string, number, integer, boolean, enum; x-secret, x-multiline) auto-generated form on the Apps, API & Webhook page (Ứng dụng, API & Webhook): click the app tile → Settings (Cài đặt); secrets are encrypted
workflowNodes { type: '<prefix>.<name>', label, configSchema, outputs } backend nodes[type](cfg, run, ctx); x-widget: account|thread|label|assistant (assistant = picks a Deplao AI assistant and stores its id; includes a "Default" (Mặc định) option = left empty), x-template
triggers { type: '<prefix>.trigger.<name>', label, configSchema } fired with deplao.workflow.trigger(type, payload); payload fields are used as {{ $trigger.<field> }}
routes { method, path, scope } /api/v1/plugins/<id><path>; the caller needs scope; body ≤ 1 MB
schedules { id, cron, handler } calls handlers[handler]; if the previous run hasn't finished, the next one is skipped
events ['booking.created'] emitted as plugin.<id>.booking.created

SDK deplao.*

Group Functions
API v1 deplao.<group>.<operationId>(params), deplao.call(operationId, params), deplao.api.request(method, path, { query, body }). Functions that return files (downloadMessageMedia, downloadLibraryItem, downloadLibraryThumb) return { name, mime, size, base64 } (≤ 25 MB, requires media:read; Zalo voice messages not yet on the machine are downloaded by the app automatically)
storage get, set, delete, list(prefix); backend: dataDir()
settings get(), onChange(cb)
events on(type, handler) (requires events:subscribe), emit(name, data)
workflow trigger(type, payload)
media upload(file) → an upload_id to use in attachments when sending a message (≤ 25 MB); the Media group's operationIds can be called like any other group
log debug/info/warn/error → plugin log (Apps, API & Webhook page → click the app tile → Log, Nhật ký)
system info()
ui (UI only) context, onContext, theme, openConversation, insertComposerText, toast, navigate(pageId), openExternal(url), onCommand
backend (UI only) call(name, params) → commands[name] in the plugin's own backend
http (backend only) fetch(url, init): requires network, blocks the local network, ≤ 5 MB; returns text or base64 depending on the Content-Type (if it is missing, the content is inspected), init.responseType = 'text' / 'base64' to force one
cloudflare (backend only) status(), d1.ensure(name), d1.migrate(name, dir), d1.query(name, sql, params), workers.get(name), workers.deployBundle({ name, bundleDir, bindings, secrets, workersDev }): requires cloudflare:workers; every call counts toward the app's budget of 100 requests / 5 minutes

Errors come back as an Error with a code (the same error codes as the API: FORBIDDEN_SCOPE, FORBIDDEN_ACCOUNT, VALIDATION_ERROR…) and a status. deplao.cloudflare adds PLUGIN_PERMISSION, CF_NOT_CONNECTED, CF_PERMISSION, CF_NAME_FORBIDDEN, CF_BUNDLE_INVALID, CF_API.

Backend (runtime local)

js
module.exports = {
  async activate({ deplao, settings, pluginId, dataDir }) {},
  async deactivate() {},                        // 5 seconds max
  async onUpgrade(prevVersion) {},
  async onSettingsChanged(settings) {},
  nodes: { 'shop.lookup': async (cfg, run, ctx) => ({ found: true }) },   // 60 seconds
  routes: { 'GET /orders/:code': async (req, ctx) => ({ status: 200, body: {} }) },   // 30 seconds
  handlers: { remind: async (ctx) => {} },     // schedules, 120 seconds
  commands: { book: async (params, ctx) => ({}) },   // UI backend.call, composer buttons
};
  • Runs in a separate process (utilityProcess, 256 MB heap) and only receives a few environment variables; require('electron') and process.exit are blocked; console.* goes to the plugin log.
  • After a crash it restarts automatically after 2 s, 10 s, 60 s; on the 4th crash the plugin is turned off ("Disabled due to error", Tắt do lỗi). If it fails to answer 3 pings in a row, it is restarted.
  • A plugin is still Node code running on the machine: Deplao only hands out data according to the approved permissions, but it does not isolate the process's file system or network access. Only install plugins from trusted sources.

Runtime remote

  • Deplao calls POST {remote.baseUrl}/rpc with the body { id, method, params } and the headers X-Deplao-Plugin, X-Deplao-Timestamp, X-Deplao-Signature: v1=HMAC-SHA256(secret, timestamp + "." + body). Respond with { result } or { error: { code, message } }.
  • method: activate, deactivate, ping, node.execute, route.handle, schedule.run, composer.command, settings.changed.
  • On install, Deplao creates a secret and an API key (bound to the plugin, revoked when it is uninstalled) and shows them once. Your server uses the key to call API v1 and to register webhooks for receiving events.
  • baseUrl must be https (http is allowed only for 127.0.0.1 during development).

Security

  • The plugin UI runs on its own origin deplao-plugin://<id> in a sandboxed iframe (no popups, no navigating the app's pages, no access to the app's DOM, store or IPC); the CSP blocks external scripts, outgoing connections except ui.connectSrc, and external images except ui.imgSrc.
  • Every data call goes through API v1 with the principal plugin:<id>: limited to the scopes and accounts granted at install time, with requests logged just like for API keys.
  • Zip packages are checked: max 50 MB, max 200 MB unpacked, no absolute paths or .., no symlinks, matching CRCs.
  • The plugin management IPC channels (plugins:*) cannot be called through the API or from a plugin. The same applies to the Cloudflare channels (cloudflare:*), except cloudflare:workers:* when the app has the cloudflare:workers permission: Deplao checks the permission and the name prefix before making the call.

Didn't find your answer?

Message us on Telegram