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:
from noxus_sdk.ncl import (
    ConfigSelect,
    ConfigToggle,
    ConfigNumberSlider,
    Parameter,
)


class GetWeatherConfig(NodeConfiguration):
    units: str = Parameter(
        default="metric",
        title="Temperature Units",
        description="Choose temperature units",
        display=ConfigSelect(options=["metric", "imperial", "kelvin"]),
    )

    include_humidity: bool = Parameter(
        default=False,
        title="Include Humidity",
        description="Also return humidity data",
        display=ConfigToggle(),
    )
Access config values in your node’s call():
async def call(self, ctx: RemoteExecutionContext, city: str) -> dict:
    units = self.config.units  # "metric", "imperial", or "kelvin"
    include_humidity = self.config.include_humidity
    # ...

Available config display types

TypeDescription
ConfigText()Single-line text input
ConfigBigText()Multi-line textarea
ConfigSelect(options=[...])Dropdown select
ConfigMultiSelect(options=[...])Multi-select dropdown
ConfigToggle()Boolean switch
ConfigNumber()Number input
ConfigNumberSlider(min, max, step)Slider
ConfigRichTextVariables()Rich text with variable insertion

Dynamic configuration

Override get_config() to generate options dynamically based on context:
class GetWeatherNode(BaseNode[GetWeatherConfig]):
    # ...

    @classmethod
    async def get_config(cls, ctx, config_response, *, skip_cache=False):
        # You could fetch available options from an API,
        # populate dropdowns based on credentials, etc.
        return config_response

List handling

When a list output connects to a non-list input, the node automatically runs once per item. If you need to process the whole list at once, declare the input as a list:
# This node receives the full list
inputs = [
    Connector(
        name="cities",
        label="Cities",
        definition=TypeDefinition(data_type=DataType.str, is_list=True),
    )
]

async def call(self, ctx, cities: list[str]) -> dict:
    # Process all cities at once
    results = []
    for city in cities:
        results.append(await fetch_weather(city))
    return {"results": results}

Plugin-level configuration

Use PluginConfiguration for settings that apply to the entire plugin (not per-node). These are set in Settings → Plugins → Configure:
class WeatherPluginConfig(PluginConfiguration):
    default_units: str = Parameter(
        default="metric",
        title="Default Units",
        display=ConfigSelect(options=["metric", "imperial"]),
    )

    cache_ttl: int = Parameter(
        default=300,
        title="Cache TTL (seconds)",
        display=ConfigNumberSlider(min=60, max=3600, step=60),
    )
Access plugin config in any node:
async def call(self, ctx: RemoteExecutionContext, city: str) -> dict:
    plugin_config = ctx.plugin_config
    default_units = plugin_config.get("default_units", "metric")
    # ...

Error handling

Raise exceptions with clear messages. The platform captures them and shows them to the user:
async def call(self, ctx, city: str) -> dict:
    if not city or not city.strip():
        raise ValueError("City name cannot be empty")

    try:
        result = await fetch_weather(city)
    except httpx.HTTPStatusError as e:
        if e.response.status_code == 404:
            raise ValueError(f"City '{city}' not found") from e
        raise RuntimeError(f"Weather API error: {e.response.status_code}") from e

    return {"temperature": result["temp"], "description": result["desc"]}
Best practices:
  • Use ValueError for input validation errors (user-fixable)
  • Use RuntimeError for unexpected failures (system errors)
  • Always chain exceptions with from e for better stack traces
  • Include actionable information in error messages

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:
noxus plugin package --path ./weather-plugin --output weather-plugin.tar.gz
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 settings for credential configuration

Complete example

Here’s the full weather_plugin/__init__.py putting everything together:
from noxus_sdk.files import File
from noxus_sdk.integrations.base import BaseIntegration, BaseCredentials
from noxus_sdk.ncl import ConfigSelect, ConfigText, ConfigToggle, Parameter
from noxus_sdk.nodes.base import BaseNode, NodeConfiguration
from noxus_sdk.nodes.connector import Connector
from noxus_sdk.nodes.types import DataType, NodeCategory, TypeDefinition
from noxus_sdk.plugins import BasePlugin, PluginConfiguration
from noxus_sdk.plugins.context import RemoteExecutionContext
from noxus_sdk.plugins.types import PluginCategory


# ── Integration ──────────────────────────────────────────────────────

class WeatherAPICredentials(BaseCredentials):
    type: str = "weather_api"
    api_key: str = Parameter(
        default="", title="API Key",
        description="OpenWeatherMap API key",
        display=ConfigText(),
    )

    def is_ready(self) -> bool:
        return bool(self.api_key)


class WeatherAPIIntegration(BaseIntegration[WeatherAPICredentials]):
    type = "weather_api"
    display_name = "Weather API"
    image = "https://cdn-icons-png.flaticon.com/512/1779/1779940.png"


# ── Node Configuration ──────────────────────────────────────────────

class GetWeatherConfig(NodeConfiguration):
    units: str = Parameter(
        default="metric", title="Units",
        display=ConfigSelect(options=["metric", "imperial"]),
    )
    include_humidity: bool = Parameter(
        default=False, title="Include Humidity",
        display=ConfigToggle(),
    )


# ── Node ─────────────────────────────────────────────────────────────

class GetWeatherNode(BaseNode[GetWeatherConfig]):
    node_name = "get_weather"
    title = "Get Weather"
    description = "Fetches current weather for a city"
    category = NodeCategory.DATA
    color = "#4A90E2"
    integrations = {"weather_api": ["api_key"]}

    inputs = [
        Connector(
            name="city", label="City",
            definition=TypeDefinition(data_type=DataType.str),
        )
    ]

    outputs = [
        Connector(
            name="temperature", label="Temperature",
            definition=TypeDefinition(data_type=DataType.str),
        ),
        Connector(
            name="description", label="Description",
            definition=TypeDefinition(data_type=DataType.str),
        ),
    ]

    async def call(self, ctx: RemoteExecutionContext, city: str) -> dict:
        import httpx

        creds = ctx.get_integration_credentials("weather_api")
        api_key = creds.get("api_key", "")
        if not api_key:
            raise ValueError("Weather API key not configured")

        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://api.openweathermap.org/data/2.5/weather",
                params={"q": city, "appid": api_key, "units": self.config.units},
            )
            response.raise_for_status()
            data = response.json()

        result = {
            "temperature": f"{data['main']['temp']}°{'C' if self.config.units == 'metric' else 'F'}",
            "description": data["weather"][0]["description"].capitalize(),
        }

        if self.config.include_humidity:
            result["description"] += f" (Humidity: {data['main']['humidity']}%)"

        return result


# ── Plugin ───────────────────────────────────────────────────────────

class WeatherPluginConfig(PluginConfiguration):
    pass


class WeatherPlugin(BasePlugin[WeatherPluginConfig]):
    name = "weather-plugin"
    display_name = "Weather Plugin"
    version = "0.1.0"
    description = "Weather data nodes with OpenWeatherMap integration"
    category = PluginCategory.GENERAL
    author = "Your Name"

    def nodes(self):
        return [GetWeatherNode]

    def integrations(self):
        return [WeatherAPIIntegration]