Docs
search Esc

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:plugin scope.

Plugin Requirements

  • Exports an async run(context) function via module.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.
warning
Blocked modulesrequire('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

PermissionData Access
reports:readLatest impact reports for schools in the org
schools:readSchool 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

  1. Go to Dev Portal → Publish Plugin or call POST /hub/api/dev/plugins.

  2. Provide identity metadata: id (lowercase letters/numbers/hyphens), name, version, category, icon, description.

  3. Select permissions and triggers.

  4. Define the settings schema.

  5. Paste the plugin code.

  6. Submit for admin review.

warning
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.