Skip to main content
This is part 6 of the Your First Plugin tutorial. Make sure you’ve completed 5. Working with Files first.

Node configuration with UI controls

Add configuration fields that appear in the node’s settings panel in the editor:
Every display widget requires a label — it has no default. The description on Parameter is optional helper text; the label on the widget is what renders as the field name.
Access config values in your node’s call():

Available config display types

Every widget takes a label; the argument column shows what else each accepts.

Dynamic configuration

Override get_config() to compute config server-side at edit time — for example, fetching a select’s options from an upstream API. It runs in the plugin’s worker; mutate the ConfigResponse and return it. Write dynamic options into config_response.config_values keyed by the field name:
The platform only wakes the worker for get_config when a ConfigSelect has no static values. Nodes with fully static config never pay the round-trip, so add get_config only when options genuinely depend on context.

Polling triggers

A plugin can also emit events on a schedule with BasePollingTrigger. The platform owns scheduling, state persistence, and turning events into runs — your trigger only implements poll(ctx, state), returning (events, new_state). Cursor state round-trips between polls so you emit each event once:
Register triggers on the plugin the same way as nodes and integrations:
Each event dict’s fields become the workflow’s trigger inputs (matching the outputs you declared). A trigger can also declare integrations = ["weather_api"] to require a connected credential.

List handling

When a list output connects to a non-list input, the platform runs the node once per item automatically. To process the whole list in a single run, declare the input as a list instead. On a V2 node that’s a bindable list[...] config field:

Plugin-level configuration

Use PluginConfiguration for settings that apply to the entire plugin (not per-node). These are set in Settings → Plugins → Configure:
Access plugin config in any node — ctx.plugin_config is a plain dict:

Error handling

Raise the SDK’s typed errors from noxus_sdk.errors. The message crosses the JSON-RPC sandbox boundary and becomes the node’s user-visible failure reason — the exception type itself does not survive, so make every message actionable:
Best practices:
  • Use IntegrationFailedError for credential/API/user-fixable problems.
  • Use UnexpectedError for failures you did not anticipate.
  • Degrade, don’t raise, for expected external flakiness. A raised exception fails the whole run. For flaky network or parsing, catch it and return an empty typed result; reserve exceptions for genuine misconfiguration (e.g. no credential connected). The Proxy API scrapers do this — a failed scrape yields an empty CSV rather than crashing the flow.
  • Chain with from e and include actionable information in the message.

Runtime & sandbox gotchas

Your code runs as a warm worker inside a sandbox VM, reached over JSON-RPC. That environment imposes a few rules the local dev server does not:
  • stdout is the protocol. The worker speaks JSON-RPC on stdout; anything you (or a dependency) print() there corrupts the channel and breaks the call. Redirect noisy code to stderr:
  • Import heavy deps lazily, inside call/helpers — not at module top. Manifest generation and noxus plugin validate import your module in a lightweight environment; a top-level import pandas / playwright / curl_cffi makes get_manifest() fail with ModuleNotFoundError even though the sandbox has the dep. Put the import in the function that uses it.
  • Degrade, don’t raise, for expected external failures (see above).
  • Don’t shadow a package with a same-named module. A weather.py next to a weather/ package makes import your_pkg.weather resolve to the package — the module becomes unreachable. Name adapters distinctly (weather_adapter.py).
  • Zip only source. Exclude .venv, __pycache__, .git, node_modules. An accidental .venv can balloon the archive from ~300 KB to tens of MB.
  • Browser / JIT plugins can’t run under the default syd sandbox. syd’s seccomp filter SIGSYS-kills V8’s JIT, so Playwright / Chromium / any Node or JIT-heavy workload dies on startup — even when you only drive a remote browser, the local Playwright Node driver still runs. Those plugins need the gvisor sandbox provider (Linux/EKS) or none (local dev — gVisor can’t pivot_root inside Docker Desktop on macOS). This is a deployment setting (SANDBOX_PROVIDER), but know it before shipping a browser plugin.
  • Keep noxus-sdk>= aligned with the platform. The sandbox resolves noxus-sdk alongside your pyproject.toml deps; pinning an older SDK can win and the worker fails to start. Depend on noxus-sdk at or above the platform’s version, not an old floor.

Deploy your plugin

Option 1: From a Git repository

Push your plugin to a Git repository, then install from the Noxus UI:
  1. Go to Settings → Plugins → Install Plugin
  2. Choose Git source
  3. Enter your repository URL, branch, and path (if the plugin is in a subdirectory)
  4. For private repos, provide an access token

Option 2: Upload directly

Package and upload:
Then upload the .tar.gz file through the Noxus UI.

Option 3: Marketplace

Publish to the Noxus plugins marketplace for public distribution.

Verify installation

Once installed, you should see:
  • Your plugin listed with status Running in Settings → Plugins
  • Your nodes available in the flow editor palette
  • Your integration available in workspace control for credential configuration

Complete example

Here’s the full weather_plugin/__init__.py putting everything together:

Examples

Browse more code examples

Architecture

Understand how plugins run under the hood