Custom Nodes v2

Build a reusable C# Node, test its behavior locally, package its Release output, and publish it to your tenant through the Console.

A custom Node is a good fit when the same capability must be reused across Workflows, centrally versioned, or governed independently. For logic used by one Workflow, start with an embedded C# Script.

Before you start

Install the following tools:

  • .NET 10 SDK
  • Visual Studio Code
  • The Microsoft C# Dev Kit extension for Visual Studio Code
  • A ZIP utility such as zip on macOS or Linux, or Compress-Archive in PowerShell

You also need permission to publish Nodes for the Account that will own the package.

Incubate the Node as a Script

A useful way to incubate a custom Node is to let an agent build it as an embedded C# Script first. The agent can help shape the Parameters, Returns, error handling, and package dependencies while you test the code in a real Workflow.

  1. Add the C# Script Node to a development Workflow.
  2. Ask the agent to implement the smallest safe version of the capability.
  3. Test the Script with non-production data and review its source and dependencies.
  4. Copy the complete Script.Content source into the main class of the new C# project.
  5. Move each script NuGet dependency into the project file, then add unit tests around the extracted behavior.

The Script and custom Node models use the same Flowgear.Sdk namespace and attributes. A published Node can then add reusable Connection types, more Methods, embedded resources, and an independent release lifecycle.

Create the project in Visual Studio Code

  1. Create an empty folder named CustomNode, then open it in Visual Studio Code.
  2. Open Terminal → New Terminal.
  3. Create the solution and class-library project:
dotnet new sln --name CustomNode
dotnet new classlib --name CustomNode --framework net10.0
dotnet sln CustomNode.sln add CustomNode/CustomNode.csproj
  1. Delete the generated CustomNode/Class1.cs file.
  2. Replace CustomNode/CustomNode.csproj with the following project file:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <AssemblyName>Acme.Flowgear.Nodes.CustomNode</AssemblyName>
    <RootNamespace>Acme.Flowgear.Nodes</RootNamespace>
    <AssemblyVersion>0.0.0.1</AssemblyVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Flowgear.Sdk" Version="2.0.0.0" />
  </ItemGroup>
</Project>

Flowgear.Sdk version 2.0.0.0 provides the public v2 Node attributes and contracts. Pin package versions so every build is repeatable. Add other dependencies with an exact version, for example:

dotnet add CustomNode/CustomNode.csproj package Example.Package --version 1.2.3

Flowgear's own Node projects use the same SDK contracts through a source project reference. A standalone project uses the NuGet package instead.

Add the Node class

Create CustomNode/NormalizeName.cs and add:

using Flowgear.Sdk;

namespace Acme.Flowgear.Nodes;

[FgNode("v2.AcmeNormalizeName", "Acme Normalize Name", SupportedClusters.Anywhere)]
public sealed class NormalizeName
{
    [FgMethod.Invoke(
        NodeTypes.Processor,
        "Execute",
        "Normalizes a supplied name.",
        "Result")]
    public Task<string> ExecuteAsync(
        [FgParameter("Name", "The name to normalize.")] string name,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        return Task.FromResult(name.Trim());
    }
}

Use these rules when you expand the class:

  • Make the Node class public and preferably sealed.
  • Give [FgNode] a permanent ID that starts with v2. and uses a PascalCase name.
  • Choose SupportedClusters.CloudRuntime, SupportedClusters.LocalRuntime, or SupportedClusters.Anywhere according to where the code can run.
  • Add [FgMethod.Invoke] to every callable Method.
  • Add [FgParameter] to every designer-visible Parameter.
  • Accept and observe a CancellationToken for asynchronous or long-running work.
  • Keep the class stateless between calls. Store persistent state in Flowgear or the external system.
  • Increment <AssemblyVersion> whenever you publish changed code, dependencies, or embedded resources. The assembly version becomes the Node version in Flowgear.

You can place several Node classes in one project, but a package must contain only one assembly that defines Nodes.

Build and test locally

Restore and build the project from the Visual Studio Code terminal:

dotnet restore CustomNode.sln
dotnet build CustomNode.sln --configuration Debug

Add an automated test project

Create an MSTest project and reference the Node project:

dotnet new mstest --name CustomNode.Tests --framework net10.0
dotnet add CustomNode.Tests/CustomNode.Tests.csproj reference CustomNode/CustomNode.csproj
dotnet sln CustomNode.sln add CustomNode.Tests/CustomNode.Tests.csproj

Create CustomNode.Tests/NormalizeNameTests.cs:

using Acme.Flowgear.Nodes;

namespace CustomNode.Tests;

[TestClass]
public sealed class NormalizeNameTests
{
    [TestMethod]
    public async Task ExecuteAsync_TrimsTheName()
    {
        var node = new NormalizeName();

        var result = await node.ExecuteAsync("  Ada Lovelace  ");

        Assert.AreEqual("Ada Lovelace", result);
    }
}

Run all tests:

dotnet test CustomNode.sln --configuration Debug

You can also run or debug an individual test from the Visual Studio Code Testing view. Unit tests should cover validation, response shapes, error behavior, cancellation, and any transformations that do not require the Runtime.

Add an optional debug harness

A small Console project is useful when you want to set breakpoints and call the Node's ordinary C# Methods directly:

dotnet new console --name CustomNode.Harness --framework net10.0
dotnet add CustomNode.Harness/CustomNode.Harness.csproj reference CustomNode/CustomNode.csproj
dotnet sln CustomNode.sln add CustomNode.Harness/CustomNode.Harness.csproj

Replace CustomNode.Harness/Program.cs with:

using Acme.Flowgear.Nodes;

var node = new NormalizeName();
var result = await node.ExecuteAsync("  Ada Lovelace  ");
Console.WriteLine(result);

This harness tests your code, but it does not emulate Runtime-provided Connections, state, logging, Step context, or template discovery. Test those integrations after publishing the package to a non-production tenant and Workflow.

Configure Visual Studio Code tasks and launch

Create .vscode/tasks.json to make the common terminal commands available through Terminal → Run Task:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build",
      "type": "process",
      "command": "dotnet",
      "args": ["build", "${workspaceFolder}/CustomNode.sln"],
      "problemMatcher": "$msCompile",
      "group": { "kind": "build", "isDefault": true }
    },
    {
      "label": "test",
      "type": "process",
      "command": "dotnet",
      "args": ["test", "${workspaceFolder}/CustomNode.sln"],
      "problemMatcher": "$msCompile",
      "group": "test"
    },
    {
      "label": "publish-release",
      "type": "process",
      "command": "dotnet",
      "args": [
        "publish",
        "${workspaceFolder}/CustomNode/CustomNode.csproj",
        "--configuration",
        "Release",
        "--output",
        "${workspaceFolder}/artifacts/publish"
      ],
      "problemMatcher": "$msCompile"
    }
  ]
}

If you added the Console harness, create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug custom Node harness",
      "type": "coreclr",
      "request": "launch",
      "preLaunchTask": "build",
      "program": "${workspaceFolder}/CustomNode.Harness/bin/Debug/net10.0/CustomNode.Harness.dll",
      "cwd": "${workspaceFolder}/CustomNode.Harness",
      "console": "integratedTerminal",
      "stopAtEntry": false
    }
  ]
}

Press F5 and select Debug custom Node harness. A class library cannot be launched by itself, so the launch configuration starts the harness.

Create the Release package

Run the tests, then publish the Node project in Release configuration:

dotnet test CustomNode.sln --configuration Release
dotnet publish CustomNode/CustomNode.csproj --configuration Release --output artifacts/publish

The ZIP file uploaded through the Console must contain the contents of artifacts/publish, including the Node assembly and its required dependencies. The Node assembly must be at the root of the ZIP file. Do not place the publish folder itself inside the archive.

On macOS or Linux:

cd artifacts/publish
zip -r ../CustomNode.0.0.0.1.zip .
cd ../..

On Windows PowerShell:

Compress-Archive -Path .\artifacts\publish\* -DestinationPath .\artifacts\CustomNode.0.0.0.1.zip -Force

Review the archive before uploading it. Remove source files, tests, local configuration, credentials, debug symbols that you do not intend to distribute, and unrelated build artifacts. The Console accepts .zip Node packages up to 100 MB.

Publish the Node to your tenant

  1. Sign in to the Console and open Settings → Nodes.
  2. Click Publish Node.
  3. Select the Account that will own the Node.
  4. Select V2 as the Runtime version.
  5. Upload the Release ZIP file. You can queue more than one package if required.
  6. Click Submit.
  7. Confirm that the success message shows the expected Node display name and assembly version.
  8. Add the Node to a development Workflow and test it with non-production Connections and data before wider use.

Publishing parses the assembly metadata and makes the Node available in the tenant. If the Console reports that the package contains no Nodes, check the [FgNode] attribute, the SDK version, and the ZIP layout. If it reports more than one Node assembly, package each Node-defining assembly separately.

Security guidance

Warning: A custom Node is trusted executable code. It is not sandboxed from the Runtime process. Publish only source and dependencies that your organization has reviewed and approved.

Apply the following controls:

  • Pin every NuGet dependency to an exact version. Review its license, provenance, vulnerabilities, and transitive dependencies before release.
  • Put credentials in a Connection, mark secret Properties as masked, and never hard-code or log secrets.
  • Validate URLs, file paths, certificates, commands, and untrusted input before using network, filesystem, or process APIs.
  • Use least-privilege service accounts and select the narrowest correct SupportedClusters value.
  • Avoid static mutable state and clean up streams, HTTP responses, and other disposable resources.
  • Log enough context to diagnose failures without recording credentials, sensitive payloads, or personal data.
  • Review the exact Release ZIP, publish it to a non-production tenant first, and retain the source, dependency lock information, test results, and version that produced it.

See also

See Security and Governance and Workflow Log Redaction for related platform controls.