Add-on code requirements

What your add-on code must and must not do to pass review and keep working across eLabNext updates.

Add-on code requirements

Every add-on submitted to the Marketplace is reviewed against the rules on this page. Check your code against them before you submit. The rules exist for three reasons: your add-on keeps working when eLabNext changes, it cannot break other add-ons or the page it runs on, and it cannot leak the data of the people who install it.

This page covers the code. The documentation, support, and listing requirements for a public add-on are on Public add-on requirements.

Structure your file

An add-on is one JavaScript file. You can author it in TypeScript or with a bundler, but the uploaded file is the compiled .js file.

The file declares one global variable, the rootVar, and wraps everything else in an immediately invoked function that receives it. The rootVar is the add-on identifier you chose when you created the add-on in the Developer Platform, and the starter template you download there already declares it:

var MY_ADDON = {};

(function (context) {
    function renderBadge(sample) {
        // helpers live here, next to init, so every handler can reach them
    }

    context.init = function (configuration) {
        eLabSDK2.Inventory.Sample.SampleDetail.registerAction({
            id: 'MY_ADDON_show_badge',
            label: 'Show badge',
            icon: 'fas fa-tag',
            onClick: function () {
                renderBadge(eLabSDK2.Inventory.Sample.SampleDetail.getSample());
            },
        });
    };
})(MY_ADDON);
  • Do declare exactly the identifier the Developer Platform shows on the Code page. The upload is refused when the code does not declare it, and eLabNext cannot load an add-on whose rootVar does not match.
  • Do define helper functions inside the wrapper but outside init, so click handlers and later init runs can call them.
  • Do put real work in init. An empty init means the add-on does nothing, and the review treats it as unfinished.
  • Don't declare anything else at the top level. No second global, no window.myHelper = .... Other add-ons and eLabNext itself share that scope.
  • Don't replace built-ins such as window.fetch or JSON.parse, and don't add methods to Object.prototype, Array.prototype, or any other native prototype. That changes behavior for every script on the page.
  • Don't write to __proto__ or constructor, and don't deep-merge objects that came from outside your add-on without filtering those keys. Use a Map or Object.create(null) when the keys come from data.

Keep the file focused. A few hundred lines is normal. A bundle approaching a megabyte is examined for what it contains.

Know when your code runs

eLabNext calls init once the document is parsed and the SDK is ready. On the inventory pages, which navigate without reloading, it calls init again on every route change, so make init safe to run more than once. Registering an action or section with an id that already exists replaces the earlier registration; elements you create yourself are not de-duplicated for you.

  • Don't wait for DOMContentLoaded, poll with setTimeout or setInterval, or guard calls with if (window.eLabSDK2). The document is parsed and the SDK is loaded before init is called.
  • Don't read the first argument of init as page data. It is your add-on's configuration (see Configure your add-on). Read page data through the SDK.

What you do inside init depends on what the add-on is for:

Your add-onIn init
Adds a button, bulk action, or section to a pageCall the SDK's register... or add... method directly. The SDK shows it when the page is there and runs your handler on click.
Needs page data as soon as a specific page opensUse that page's ready callback, for example eLabSDK2.Inventory.Sample.SampleDetail.onSampleDetailReady(callback, 'MY_ADDON_ready'). Fall back to eLabSDK2.onAfterPageLoad(callback, 'MY_ADDON_ready') only when no page-specific callback exists.
Shows a floating panel or timer on every pageCreate it right away. Ready callbacks may never fire for a page that has no such event.
// Wrong: runs at load time, probably before the user opens a sample
context.init = function () {
    var sample = eLabSDK2.Inventory.Sample.SampleDetail.getSample();
    renderBadge(sample);
};

// Right: runs when the sample detail page is ready
context.init = function () {
    eLabSDK2.Inventory.Sample.SampleDetail.onSampleDetailReady(function () {
        renderBadge(eLabSDK2.Inventory.Sample.SampleDetail.getSample());
    }, 'MY_ADDON_ready');
};

Timers are allowed when timing is the feature, such as a countdown or a clock. They are not allowed as a way to wait for the page or for data.

Read and write eLabNext data

  • Do use only functions that exist in the SDK reference and endpoints that exist in the API reference. A function or endpoint that is not in the documentation is not supported.
  • Do prefer SDK2 when both SDKs offer the same function. An add-on can use SDK1 and SDK2 together.
  • Do await the SDK calls that return a Promise, such as reloadSample, and call the ones that don't, such as getSample, as plain functions. The reference shows the return type of each.
async function refreshQuantity(sampleID) {
    await eLabSDK2.Inventory.Sample.SampleDetail.reloadSample(sampleID);
    var sample = eLabSDK2.Inventory.Sample.SampleDetail.getSample();
    eLabSDK2.UI.Toast.showToast('Reloaded ' + sample.name, 5000);
}

Calling the REST API from an add-on

eLabSDK.API.call sends the request as the logged-in user, so you never handle tokens or base URLs. The full guide is API usage in an add-on; the rules that matter for review are:

  • Do give path a relative path such as samples/123/meta. The SDK prepends the API base URL of the user's environment, so a full URL produces a broken request.
  • Do wrap the call in a Promise before you await it. The function is callback-based; awaiting it directly gives you the request object, not the response data.
  • Don't call the eLabNext API with fetch, XMLHttpRequest, or a library such as axios, and don't build an Authorization header yourself. That ties your add-on to today's session mechanics and breaks when they change.
  • Don't use eLabSDK.API.call to read data the SDK already gives you. Read through SDK getters, write through the API.
function fetchSampleLogs(sampleID) {
    return new Promise(function (resolve, reject) {
        eLabSDK.API.call({
            method: 'GET',
            path: 'samples/' + sampleID + '/logs',
            queryParams: { $records: 100 },
            onSuccess: function (xhr, status, response) {
                resolve(response);
            },
            onError: function (xhr, status, response) {
                reject(new Error('Could not load sample logs'));
            },
        });
    });
}

The body of a PUT or POST to samples/{id}/meta uses key, value, and sampleDataType, not sampleMetaKey and sampleMetaValue. The type of a number field is NUMERIC; INTEGER is not a valid type.

Keep API usage proportionate to the task. Paging through an entire inventory to find one sample is replaced with a filtered query on review.

Work with the page

The page belongs to eLabNext. Element IDs, class names, and structure change between releases, and the SDK is what keeps your add-on working through those changes.

  • Do add buttons, sections, tabs, dialogs, and toasts through the SDK: registerAction, addSection, addTab, eLabSDK2.UI.Dialog, eLabSDK2.UI.Toast.
  • Don't read or change elements eLabNext rendered. document.getElementById('sampleHeader').innerHTML = ... breaks on the next release and is rejected on review.

Elements you create yourself

You may create your own elements, for example a floating timer, and append them to the page. When you do:

  • Do prefix every ID, class name, and style element with your rootVar: MY_ADDON_panel, .MY_ADDON_button, MY_ADDON_styles. Generic names collide with other add-ons.
  • Do check that the element does not already exist before you create it. init runs again on navigation, and without the check you get duplicates.
  • Do set position: fixed and the coordinates before you append a floating element, and make sure document.body exists.
  • Do check an element exists before you read its properties. A missing element throws and stops your add-on.
function ensureStyles() {
    if (document.getElementById('MY_ADDON_styles')) {
        return;
    }
    var style = document.createElement('style');
    style.id = 'MY_ADDON_styles';
    style.textContent = '.MY_ADDON_panel { position: fixed; top: 20px; right: 20px; }';
    document.head.appendChild(style);
}

Binding events in dialogs and sections

Dialog and section content is an HTML string that eLabNext renders for you. The elements inside it exist only after rendering, so attach your event listeners in the onRendered callback, never after a setTimeout:

eLabSDK2.UI.Dialog.showDialog({
    id: 'MY_ADDON_dialog',
    title: 'Enter a barcode',
    content: '<input id="MY_ADDON_barcode" type="text" />',
    onRendered: function () {
        var input = document.getElementById('MY_ADDON_barcode');
        if (input) {
            input.addEventListener('change', function () {
                lookUp(input.value);
            });
        }
    },
});

addSection on the inventory detail pages accepts the same onRendered callback.

Choosing the right surface

You wantUse
Input from the user, a confirmation, or an error that blocks the pageeLabSDK2.UI.Dialog.showDialog, showConfirmDialog, showErrorDialog
A short status messageeLabSDK2.UI.Toast.showToast
Content that belongs to a sample, storage unit, or experimentaddSection or addTab on that page's SDK class; link it to related content with position where supported
A panel that is always visible, independent of the pageYour own fixed-position element

Use libraries

  • Do use open-source libraries with a permissive license (MIT, Apache, BSD). Commercial or proprietary libraries are not accepted.
  • Don't use jQuery or MooTools, and don't write MooTools-style new Class({...}) definitions. Both are being phased out of the platform, and the older examples that used them are outdated.
  • Don't paste a library's source into your file. Load it from a CDN, or bundle it with a build tool.

A bundle produced by a build tool, framework runtime included, is accepted. Utility libraries such as a Markdown parser or a date library are better loaded from a CDN, which keeps your file small and lets add-ons share one copy.

When you load from a CDN:

function loadMarked() {
    return new Promise(function (resolve, reject) {
        if (typeof marked !== 'undefined') {
            resolve();
            return;
        }
        var script = document.createElement('script');
        script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js';
        script.integrity = 'sha384-...';
        script.crossOrigin = 'anonymous';
        script.onload = resolve;
        script.onerror = function () {
            reject(new Error('Could not load marked'));
        };
        document.head.appendChild(script);
    });
}
  • Do check whether the library is already present. Another add-on may have loaded it.
  • Do pin the version in the URL and set integrity and crossOrigin, so a changed or compromised file fails to load instead of running.
  • Do handle onerror. A library that fails to load should produce a clear error, not a crash somewhere later.
  • Don't compute the script URL at runtime. A src built from configuration or user input is the same risk as eval.

Keep it secure

Your add-on runs with the permissions of whoever installed it. A violation of any of these rules is a rejection.

  • Don't put API keys, tokens, passwords, or other secrets in the source. The file is readable by every user of the add-on. Use add-on configuration with a password field, or OAuth.
  • Don't use eval, new Function, document.write, or the string forms of setTimeout and setInterval. They turn data into code.
  • Don't put data from the API, a form, the page, or your configuration into innerHTML or into dialog and section content without sanitizing it. Set textContent, build the elements with createElement, or run the string through a sanitizer such as DOMPurify.
  • Don't read document.cookie, localStorage, or sessionStorage and send the value anywhere. Session data stays in the browser.
  • Don't listen for message events without checking event.origin, and don't postMessage with '*' as the target origin.
// Wrong: a sample name that contains <script> runs on the page
cell.innerHTML = '<b>' + sample.name + '</b>';

// Right: the browser escapes it
var bold = document.createElement('b');
bold.textContent = sample.name;
cell.appendChild(bold);

Calling your own or a third-party service is allowed. Keep the credential in add-on configuration, send it in a header rather than in the URL, and expect the reviewer to ask where it is stored and how a user can revoke it.

Handle errors and give feedback

  • Do give every API and SDK call an error path, and do something in it. At minimum console.error(error); for anything the user triggered, also show a toast or an error dialog. An empty catch or an empty onError hides the failure from the user and from you.
  • Don't use alert(). Use eLabSDK2.UI.Dialog.showErrorDialog or eLabSDK2.UI.Toast.showToast.
  • Don't navigate with window.location.href. window.location.reload() after a data change is fine.
  • Do show a readable property of an object, such as name or label. [object Object] in the interface is a bug.
  • Do give every registered action a Font Awesome icon, such as 'fas fa-tag', that matches what it does.
try {
    await checkOut(sampleIDs);
    eLabSDK2.UI.Toast.showToast('Checked out ' + sampleIDs.length + ' samples', 5000);
} catch (error) {
    console.error(error);
    eLabSDK2.UI.Dialog.showErrorDialog('Check-out failed', error.message);
}

Configure your add-on

User-editable settings go through the configuration schema, not through hardcoded values or your own storage. The schema and default values are uploaded separately in the Developer Platform and arrive merged as the first argument of init. Name that parameter configuration and read it directly. The platform has already merged the user's values with the defaults, so don't merge them again. Side-loading passes no configuration, and Add-on configuration shows the fallback for that.

Before you submit

  • One .js file, the rootVar from the Developer Platform, everything else inside the wrapper, init does real work.
  • No other globals, no changes to built-ins or native prototypes.
  • No DOMContentLoaded, no setTimeout to wait, no if (window.eLabSDK2) guards.
  • Page data comes from SDK getters, not from the init argument.
  • Every SDK or API function you call exists in the reference.
  • eLabSDK.API.call uses relative paths, a Promise wrapper, and an onError that does something.
  • No fetch to the eLabNext API, no handmade Authorization header.
  • Sample metadata uses key, value, sampleDataType, and NUMERIC.
  • No reads or writes to elements eLabNext rendered.
  • Your own IDs, classes, and styles are prefixed with the rootVar and created only once.
  • Dialog and section events are bound in onRendered.
  • Libraries are open-source, pinned with integrity when loaded from a CDN, and never jQuery or MooTools.
  • No secrets in the source, no eval, no unsanitized innerHTML, no cookie or storage reads that leave the browser.
  • Errors are logged and shown; no alert(), no window.location.href.
  • Every action has an icon.
  • Settings come from the configuration schema.

Troubleshooting

SymptomCause / fix
The add-on never does anythinginit ran before the user reached the page. Use a ready callback or a register... method, see Know when your code runs.
await eLabSDK.API.call(...) returns an object with readyStateYou awaited the call directly. Wrap it in a Promise, see Calling the REST API from an add-on.
Panels or styles appear twice after navigatinginit ran again. Check for an existing element before creating it.
A metadata update returns 200 but nothing changesThe body used sampleMetaKey/sampleMetaValue or the type INTEGER. Use key, value, sampleDataType, and NUMERIC.
A library is undefined when your code runsThe script tag was appended but not awaited. Resolve a Promise in onload and wait for it before you use the library.

Did this page help you?