Developers
Write actions
Actions, their settings, summaries, values for later steps, errors people can act on, and what the context gives you.
An action, from start to end
This is the volume action from the Media controls sample. Every action has the same three parts: a category, an input class with the settings, and a class that derives from NimblioAction<TInput>.
using Nimblio.Sdk;
[assembly: NimblioCategory("media", "Media", Icon = "play", Color = "#E86FA6")]
namespace MediaControls;
public enum VolumeChange
{
[Option("Up")] Up,
[Option("Down")] Down,
}
public sealed class VolumeInput
{
[Field("Change", Kind = FieldKind.Segmented)]
public VolumeChange Change { get; set; } = VolumeChange.Up;
[Field("Steps", Kind = FieldKind.Slider, Min = 1, Max = 25, Help = "Each step is 2% of the volume.")]
public int Steps { get; set; } = 5;
}
[NimblioAction("change_volume", Name = "Change volume by steps", Category = "media", Icon = "volume",
Description = "Turns the volume up or down a few steps, like the volume keys on a keyboard.",
Keywords = "sound louder quieter audio", Summary = "Turn the volume {change} by {steps}")]
public sealed class ChangeVolume : NimblioAction<VolumeInput>
{
public override Task<ActionResult> RunAsync(VolumeInput input, IActionContext context, CancellationToken ct)
{
context.Require(Permissions.System);
var key = input.Change == VolumeChange.Up ? MediaKeys.VolumeUp : MediaKeys.VolumeDown;
for (var i = 0; i < input.Steps; i++) { ct.ThrowIfCancellationRequested(); MediaKeys.Press(key, context); }
return Task.FromResult(ActionResult.Done($"Turned the volume {(input.Change == VolumeChange.Up ? "up" : "down")} {input.Steps} step(s)"));
}
}In Nimblio, this becomes a Change volume by steps action in a pink Media category. Its settings are an Up/Down switch and a slider from 1 to 25. On the step card it reads “Turn the volumeUp by 5”.
Nimblio creates a new instance of your action class for every run of the step. Keep state that must last between steps in your extension class, a static field, or the data folder.
The [NimblioAction] attribute
| Property | What it does |
|---|---|
id | The first argument. Lower-case letters, digits, - or _. Unique in the extension. Never change it once published: workflows store it. |
Name | Shown in the library and on the step card. Start with a verb: “Post a message”. |
Category | A category from [NimblioCategory], or a built-in one: files, apps, browser, text, conditions, variables, notify, windows. |
Icon | A Nimblio icon name, such as play, message, globe or folder, or one of your own (see icons). |
Description | One or two sentences in the library’s preview and the inspector. |
Keywords | Extra words people might search for, separated by spaces. |
Summary | The sentence on the step card. {key} shows a setting’s value as a pill; empty required settings show as an amber prompt. |
Output | The name of the value the action gives later steps, such as “Track name”. |
TimeoutSeconds | Time limit for one run of the step, 1 to 43,200. Left at 0, the person’s setting applies. |
Destructive | Set it when the action can permanently remove data. Nimblio then asks before the run, like it does for its own delete actions. |
Categories
Declare your categories on the assembly with [assembly: NimblioCategory("id", "Name", Icon = "…", Color = "#RRGGBB")]. The colour tints the category’s icon tiles. Category ids only need to be unique inside your extension: Nimblio keeps two extensions’ “media” categories apart.
Settings
Each property of the input class marked with [Field("Label")] becomes a setting. Its key is the property name in camelCase (WebhookUrl → webhookUrl), which is what Summary,ShowWhen and field: in errors refer to. Its default is the property’s initial value.
public enum Channel { General, Announcements }
public sealed class PostInput
{
// string → text box. Tokens lets people insert {Files}, {Clipboard} or values from earlier steps.
[Field("Message", Kind = FieldKind.Textarea, Tokens = true, Required = "Write the message to post.")]
public string Message { get; set; } = "";
// enum → drop-down. [Option] on each member sets its label.
[Field("Channel")]
public Channel Channel { get; set; } = Channel.General;
// bool → switch, shown under “More settings”.
[Field("Mention everyone", Advanced = true)]
public bool MentionEveryone { get; set; }
// Shown only while Channel is Announcements.
[Field("Pin the message")]
[ShowWhen(nameof(Channel), nameof(Channel.Announcements))]
public bool Pin { get; set; }
// A whole list value, such as {Files} from “Get files in folder”, arrives as one item per entry.
[Field("Attachments", Tokens = true, Optional = true, Help = "Insert a list of files, or one path per line.")]
public List<string> Attachments { get; set; } = new();
[Field("Webhook URL", Kind = FieldKind.Url, Mono = true, Advanced = true,
Regex = "^https://", RegexMessage = "Use an https:// address.")]
public string WebhookUrl { get; set; } = "";
}Kinds of settings
Left alone, the property type picks the control: string → text, bool → switch, numbers → number, enum → drop-down. Set Kind for anything else:
| Kind | Shows | Good for |
|---|---|---|
Text | One-line text box | Names, short messages |
Textarea | Multi-line text box | Messages, notes, lists of items |
Url | Text box that completes https:// | Web addresses |
Path | Text box with a Browse button; PathKind is folder, file or save | Files and folders |
Select | Drop-down | Several choices, or long labels |
Segmented | Row of buttons | Two or three short choices |
Toggle | Switch | On/off |
Number | Number box, with Min, Max and Unit | Counts, delays |
Slider | Slider, with Min, Max and Unit | Levels, percentages |
Field options
Help,Placeholder- A line under the setting, and grey text in an empty box.
Required = "message"- Makes the setting required. While it’s empty, Nimblio shows the message on the setting and lists it in the workflow’s issues.
Regex,RegexMessage- Checks the text against a regular expression and shows the message when it doesn’t match.
Optional,Advanced- Marks the setting as optional, or puts it under “More settings”.
Tokens- Adds Insert value, so people can use values from earlier steps. Nimblio fills them in before your action runs: you always receive plain text.
Mono- Uses a fixed-width font, for code, ids and addresses.
[Option("value", "Label")]- Choices for a
SelectorSegmentedstring property. On enum members,[Option("Label")]only sets the label. [ShowWhen(nameof(Other), "value")]- Shows the setting only while another one has that value; add
Not = truefor the opposite.
A List<string> property receives a list value, such as Files, item by item. When someone types text instead, each line is one item.
Results and values for later steps
RunAsync returns an ActionResult. Its log line appears in the run log and Run history, so write it as a sentence about what happened: “Posted the message to #general”.
ActionResult.Done(log): nothing to hand on.ActionResult.WithOutput(log, text): a value later steps can insert, named byOutput.ActionResult.WithList(log, items): a list later steps can insert or repeat over.
[NimblioAction("current_track", Name = "Get the current track", Category = "spotify", Icon = "music",
Output = "Track name", Summary = "Get the track playing in Spotify")]
public sealed class CurrentTrack : NimblioAction<NoInput>
{
public override async Task<ActionResult> RunAsync(NoInput input, IActionContext context, CancellationToken ct)
{
var track = await Spotify.GetCurrentTrackAsync(ct);
return ActionResult.WithOutput($"Playing “{track}”", track);
}
}
// A list that later steps can insert, or repeat over with “Repeat for each”:
return ActionResult.WithList($"Found {names.Count} playlists", names);Errors people can act on
Throw ActionException when something goes wrong that the person can understand. Nimblio shows it in the run’s error panel with what happened and how to fix it, and Fix in editor opens the step. Pass field to point at the setting that’s wrong.
if (!Spotify.IsRunning)
throw new ActionException(
title: "Spotify isn't running",
detail: "This action talks to the Spotify app on this PC.",
fix: "Open Spotify, then run the workflow again.",
code: "SPOTIFY_NOT_RUNNING");
if (!Uri.TryCreate(input.WebhookUrl, UriKind.Absolute, out _))
throw new ActionException("The webhook address isn't valid", fix: "Copy it again from Discord's channel settings.",
field: "webhookUrl");Any other exception becomes a general “The extension ran into a problem” error (EXTENSION_ERROR) with your exception’s message, so catch what you can explain. Write the title as what happened, not an error code, and the fix as something the person can do now.
What the context gives you
IActionContext is passed to every run:
| Member | What it does |
|---|---|
Report(text) | Adds to this step’s entry in the run log and Run history, before your result’s log line. |
Log(text) | Writes to the extension’s log file, and to the console while you run nbext dev. |
NotifyAsync(title, body, sound) | Shows a Windows notification. Needs notifications. |
ShowMessageAsync(text) | Shows a message and waits for OK. |
AskAsync(question, suggested) | Asks for text; null if the person closed the dialog. |
GetClipboardAsync(), SetClipboardAsync(text) | Reads or sets the clipboard text. Needs clipboard. |
Secrets | Get, Set and Remove for tokens and passwords, encrypted for the current Windows user. |
DataFolder | A folder only your extension uses, kept across updates and uninstalls. |
Require(permission) | Stops the step with a clear error if extension.json doesn’t declare the permission. |
GetList(key) | The items of a setting, as for a List<string> property. |
WorkflowName, StepId, ExtensionId, Permissions | Where the step runs and what the person accepted. |
public override async Task<ActionResult> RunAsync(PostInput input, IActionContext context, CancellationToken ct)
{
context.Require(Permissions.Network); // fails clearly if extension.json doesn't declare it
var token = context.Secrets.Get("token");
if (token == null)
{
token = await context.AskAsync("Paste your Discord bot token");
if (token == null) throw new ActionException("No token was entered", fix: "Run the workflow again and paste the token.");
context.Secrets.Set("token", token); // encrypted for this Windows user
}
context.Report($"Posting to #{input.Channel}"); // an extra line in Run history
context.Log("Using the bot API"); // the log file and the nbext dev console
await Discord.PostAsync(token, input.Message, ct); // pass ct on, so Stop works
return ActionResult.Done("Posted the message");
}Pass the CancellationToken on to everything that waits. When someone presses Stop, or the step reaches its time limit, the token is cancelled and Nimblio moves on. Code that ignores the token keeps running in the background until it finishes, so check it in loops and long waits.
Starting and stopping
To connect to a device or a service once instead of on every step, add one class that derives fromNimblioExtension. ActivateAsync runs when the extension’s host process starts, andDeactivateAsync when it stops: after 5 idle minutes, on reload, or when Nimblio quits.
public sealed class SpotifyExtension : NimblioExtension
{
internal static SpotifyClient? Client;
public override async Task ActivateAsync(IExtensionContext context)
{
Client = await SpotifyClient.ConnectAsync(context.Secrets.Get("refresh-token"));
context.Log("Connected to Spotify.");
}
public override Task DeactivateAsync() => Client?.DisposeAsync().AsTask() ?? Task.CompletedTask;
}Settings and summaries come from attributes at build time, so they can’t depend on data at run time, such as a list of your playlists. Use a text setting with Tokens = true, and explain in Help what to type.