SophonaPluginTemplate 1.0.2
dotnet add package SophonaPluginTemplate --version 1.0.2
NuGet\Install-Package SophonaPluginTemplate -Version 1.0.2
<PackageReference Include="SophonaPluginTemplate" Version="1.0.2" />
<PackageVersion Include="SophonaPluginTemplate" Version="1.0.2" />
<PackageReference Include="SophonaPluginTemplate" />
paket add SophonaPluginTemplate --version 1.0.2
#r "nuget: SophonaPluginTemplate, 1.0.2"
#:package SophonaPluginTemplate@1.0.2
#addin nuget:?package=SophonaPluginTemplate&version=1.0.2
#tool nuget:?package=SophonaPluginTemplate&version=1.0.2
SophonaPluginTemplate
Nodes for Sophona Studio (RobotForce), built against RobotForce.Sophona.PluginConnector 1.0.18.
| Node | Category | What it does |
|---|---|---|
| Hello World | System | Returns a greeting - the smallest complete node, copy it to start a new one. simulateError = Yes produces an error on purpose to try the throwOnError field. |
| HTTP GET (text) | Apps & integrations | Downloads a page or an API response as text; shows every kind of failure a workflow (and the agent running it) must understand and the throwOnError field (stop the workflow or keep going). |
Build, test, install
dotnet build -c Release # also creates bin\Release\SophonaPluginTemplate.1.0.0.nupkg
dotnet test # tests\SophonaPluginTemplate.Tests
In Sophona Studio click the package icon in the status bar → Package Manager → drop the .nupkg
(or Upload .nupkg). The nodes appear without a restart. To publish a new build, raise <Version> first.
What the Package Manager checks
| Rule | Why | Code |
|---|---|---|
Exactly one class named Commands, derived from RobotConnector, with public Commands(object init) |
RobotExecutor finds the class by name and creates it through that constructor | PKG034, PKG035 |
Connector referenced as Version="[1.0.18]" |
RobotExecutor loads the Studio's connector, not the plugin's | PKG003, PKG004 |
<RootNamespace> not used by another plugin |
two plugins with one namespace hide each other's nodes | PKG005 |
<AssemblyName> equal to <PackageId> |
the library is named after the package (robot_modules\<id>.robotforce) |
PKG015 |
every [RobotInstruction] method has a static tile class with the same name and a frontendHtml field |
without the tile the node is not shown in the Actions panel | PKG022, PKG023 |
| node definitions load without exceptions | a throwing tile hides only that node, the rest still installs | PKG012 |
Errors block the installation and say what to change; warnings are shown and the package installs.
Adding a node
- Copy
SophonaHelloWorld.cs, rename the tile class and the method to the same name. descriptionof the tile andtooltipof every field are for the person who builds the workflow - they are shown in the Actions panel and in the node settings: what the node does, what it returns, allowed values, format, default. The agent never sees them (see below).- Keep the
[RobotInstruction]method thin - it reads its fields and callsRun(...). Every decision goes into aninternal static XxxCore(string …)method returningToolResult, which the tests call directly:
[RobotInstruction]
public Task<object> CountWords(/* fields… */ ScriptVariable text, ScriptVariable outputVariable)
{
return Run(nameof(CountWords), outputVariable, () => CountWordsCore(ToolKit.Text(text)));
}
internal static ToolResult CountWordsCore(string text)
{
if (string.IsNullOrWhiteSpace(text))
return ToolResult.Error("text is empty.", "Pass the text to count, e.g. the output of ReadFile.");
var words = text.Split((char[])null, StringSplitOptions.RemoveEmptyEntries).Length;
return ToolResult.Ok(words.ToString());
}
- Nodes run synchronously - RobotExecutor does not await the returned
Task. Asynchronous APIs are called throughTask.Run(...).GetAwaiter().GetResult()(seeHttpGetText.cs). - Once a node is used in workflows, do not change its fields (add, remove, reorder). RobotExecutor passes
the saved values by position - such workflows fail with
Parameter count mismatchuntil the node is dropped on them again. Need another field? Add a new node.
Results a workflow and an agent understand
Who sees what:
| Who | Sees |
|---|---|
| the person who builds the workflow | node title, description, field tooltips, the robot console |
| an agent (LLM) | not the nodes - it runs a whole skill as a tool, chosen by the skill's description and filled through its input variables; it gets back the workflow variables and, when a step failed, the error message of the run (Execution failed at step …: <exception message of the node>) |
So a node reaches the agent only through the text it writes into its output variable (when that variable
is, or feeds, what the skill returns) and through its exception message. ToolKit.cs implements the
contract for both:
| Situation | Return | Output variable | Step |
|---|---|---|---|
| success | ToolResult.Ok(text) |
the result; large or partial results start with a one-line header | succeeds |
| nothing found | ToolResult.Empty("what was searched, where") |
EMPTY: … |
succeeds |
| wrong input, failure | ToolResult.Error("what happened, naming the value.", "what to do next.") |
ERROR: … |
fails (ToolException) |
the same, node field throwOnError = No |
(the same ToolResult.Error) |
ERROR: … |
succeeds - the workflow goes on |
| a bug nobody foresaw | (any exception) | ERROR: unexpected failure: … with the exception chain |
fails, original exception attached |
Write messages for someone who cannot see the screen:
| Instead of | Write |
|---|---|
Operation failed |
ERROR: C:\data\report.xlsx does not exist. Use ListDirectory to find the file - do not guess paths. |
Invalid mode |
ERROR: mode: unknown value "Markdown". Allowed values: Text \| Raw. Call the node again with one of them. |
| (20 000 characters without comment) | …text… + [NOTE: showing 20000 of 81532 characters - raise maxChars or fetch a more specific page] |
ERROR: no results |
EMPTY: no file in C:\src matches *.cs |
Say when not to retry (401, 404, invalid certificate) - otherwise an agent that runs the skill will try variations of the same call.
Stop the workflow or keep going (throwOnError)
Some workflows must stop at the first error; others handle errors themselves (a loop over 500 addresses
should not die on the first 404). HelloWorld and HttpGetText have the field that lets the workflow author
choose - the same idea as throwOnNonZero of RunPowerShell in SystemActions. Put it right before the output
variable:
[RobotFieldConfig(
fieldType = typeof(string),
isDropdown = true,
dropdownArray = new string[] { "Yes", "No" },
defaultValue = "Yes",
tooltip = "Allowed values: Yes | No. Yes = an error stops the workflow. " +
"No = the workflow continues and the output variable starts with \"ERROR:\". Optional, default Yes."
)]
ScriptVariable throwOnError,
// …
return Run(nameof(MyNode), outputVariable, () => MyNodeCore(ToolKit.Text(input)),
stopWorkflowOnError: ToolKit.StopOnError(ToolKit.Text(throwOnError)));
throwOnError |
Error described by the node (ToolResult.Error, ToolException) |
Output variable | Robot console |
|---|---|---|---|
| Yes (default, also empty or unknown) | the step fails, the workflow stops | ERROR: … |
Execution failed at step …: HelloWorld: … |
| No | the step succeeds, the workflow goes on | ERROR: … - the next step checks it, e.g. result.StartsWith("ERROR:") |
⚠️ HelloWorld: … → throwOnError = No: the workflow goes on, … |
Two things stop the workflow even with No, because the error would otherwise be lost: an unexpected
exception (a bug) and a node without an output variable. A value typed into the output field instead of a
picked variable counts as no variable - RobotExecutor keeps it in an internal temp_… variable.
Without an output variable throwOnError makes no difference: an error stops the workflow, no error
leaves it green.
Try it on a workflow
Hello World has a simulateError field (No | Yes, default No) just for this - Studio fills an empty
name with the default World, so clearing the name does not produce an error.
- Install the plugin and drop Hello World on a workflow (a node added before this field existed must be
added again). In the Variables panel create a string variable
resultand pick it as the output variable. - Set simulateError = Yes, throwOnError = Yes and run: the run stops at Hello World with
HelloWorld: simulated error (simulateError = Yes) …. - Switch to throwOnError = No and run again: the step is green, the robot console shows the ⚠️ line and
resultholdsERROR: simulated error …. - Add an If after Hello World with the condition
result.StartsWith("ERROR:")- one branch handles the error, the other one uses the greeting. Set simulateError = No to see the second branch. - Clear the output variable: with simulateError = Yes the run stops for both values of throwOnError.
Dependencies
Add other NuGet packages with a normal <PackageReference>; the Package Manager installs them together
with the plugin and checks version conflicts with other plugins. Packages Sophona Studio already
contains (the .NET runtime, the connector and its dependencies) are not copied - use the versions the
Studio has or older ones.
Publishing on nuget.org
dotnet nuget push bin\Release\SophonaPluginTemplate.1.0.0.nupkg -k <api key> -s https://api.nuget.org/v3/index.json
The Browse tab lists packages owned by the nuget.org account set in packageManager:pluginOwner
(default RobotForce). Keep the sophona-plugin tag. A version on nuget.org cannot be replaced - publish
a new one.
A new plugin from this template
dotnet new install RobotForce.Sophona.Plugin.Template # or: dotnet new install <path to this folder>
dotnet new sophona-plugin -n Acme.InvoiceNodes # --IncludeTests false = without the test project
The name becomes the project, the namespace, the package id and the library name.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0-windows7.0 is compatible. net9.0-windows was computed. net10.0-windows was computed. |
-
net8.0-windows7.0
- RobotForce.Sophona.PluginConnector (= 1.0.18)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.