Publishing Plugins
Plugins extend UbuntuPlay organisations with automation — export data to Google Sheets, POST webhooks, generate reports, and run on schedules. Plugins run server-side on the hub with access to permitted data.
Prerequisites
- An active developer account (see Getting Access).
- Comfortable with async JavaScript / Node.js patterns.
- An API key with the
publish:pluginscope.
Plugin Requirements
- Exports an async
run(context)function viamodule.exports. - Returns a plain object shown as the result in the org portal.
- Uses only allowed built-in modules:
https,http,crypto,url,querystring,stream,buffer,util,events,path.
Blocked modules
require('fs'), require('child_process'), process.env, eval, and dynamic Function construction are blocked. Use the settings context for credentials instead of environment variables.The Context Object
JAVASCRIPT
async function run(context) {
const {
settings, // org-configured values from your settings schema
reports, // latest report per school (if reports:read permission)
schools, // basic school info (if schools:read permission)
orgId, // ID of the org running this plugin
orgName, // display name of the org
log, // log(msg) — writes to server console for debugging
} = context;
// reports[] structure:
// {
// hubMeta: { schoolId, country, receivedAt },
// keyMetrics: {
// uniqueLearners, totalPuzzleCompletions, quizAccuracyPercent,
// totalLearningMinutes, avgMinutesPerLearner, activeDays
// },
// breakdown: { byGame, byGrade }
// }
// schools[] structure:
// { id, schoolName, country, plan, status, studentCount, lastMetrics }
return { rowsWritten: 42, status: 'success' };
}
module.exports = { run };
Permissions & Triggers
| Permission | Data Access |
|---|---|
reports:read | Latest impact reports for schools in the org |
schools:read | School names, countries, plans, statuses, counts |
external:* | Permitted to call external HTTPS endpoints |
Triggers determine when the plugin runs:
manual— org admin clicks ▶ Run Now.schedule:daily— runs automatically at 06:00 server time.on_report— fires each time a school sends an impact report.
Settings Schema
Define configuration fields that org admins fill in. Mark sensitive values as secret: true to mask them in the UI:
JAVASCRIPT
settingsSchema: [
{
key: 'webhookUrl',
label: 'Webhook URL',
type: 'text',
required: true,
help: 'The URL to POST data to',
},
{
key: 'authToken',
label: 'Authorization Token',
type: 'text',
required: false,
secret: true,
help: 'Bearer token for API authentication',
},
{
key: 'mode',
label: 'Export Mode',
type: 'select',
options: ['append', 'replace'],
required: false,
},
]
Example: Webhook Plugin
JAVASCRIPT
const https = require('https');
async function run(context) {
const { settings, reports, schools, log, orgName } = context;
if (!settings.webhookUrl) throw new Error('Webhook URL not configured');
const payload = {
org: orgName,
timestamp: new Date().toISOString(),
schools: (reports || []).map(r => ({
name: (schools || []).find(s => s.id === r.hubMeta?.schoolId)?.schoolName,
learners: r.keyMetrics?.uniqueLearners,
minutes: r.keyMetrics?.totalLearningMinutes,
}))
};
const body = JSON.stringify(payload);
log(`Sending to ${settings.webhookUrl}`);
await new Promise((resolve, reject) => {
const url = new URL(settings.webhookUrl);
const req = https.request({
hostname: url.hostname,
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
...(settings.authToken ? { Authorization: 'Bearer ' + settings.authToken } : {})
}
}, res => res.statusCode < 400 ? resolve() : reject(new Error('HTTP ' + res.statusCode)));
req.on('error', reject);
req.write(body); req.end();
});
return { sent: true, schoolsIncluded: payload.schools.length };
}
module.exports = { run };
Submitting a Plugin
Go to Dev Portal → Publish Plugin or call
POST /hub/api/dev/plugins.Provide identity metadata:
id(lowercase letters/numbers/hyphens),name,version, category, icon, description.Select permissions and triggers.
Define the settings schema.
Paste the plugin code.
Submit for admin review.
Code changes require re-reviewAny update to plugin code triggers a re-review, even if the plugin was previously approved. Metadata-only changes preserve approval status.