Our workflow engine is designed to be more than just a pre-defined set of nodes. We recognize that every business, every API, and every integration is unique. That is why we built Vyshyvanka around a robust, assembly-based plugin architecture that allows you to extend the engine without touching the core codebase.
The Foundation: Assembly Loading
At the heart of our extensibility model is .NET's AssemblyLoadContext. When the engine starts, the PluginLoader scans a designated plugin directory for compiled .dll files. Each plugin gets its own collectible PluginLoadContext, which provides full isolation from the core application — preventing version conflicts between your dependencies and the engine's dependencies.
internal class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
// Try to resolve from plugin directory first
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath is not null)
return LoadFromAssemblyPath(assemblyPath);
// Fall back to default context for shared assemblies (like Vyshyvanka.Core)
return null;
}
}
The isCollectible: true flag means plugins can be unloaded at runtime — enabling hot-reload scenarios and safe removal of installed packages.
Declaring Plugins
Each plugin is identified by a PluginAttribute on the assembly. This tells the engine who you are, what version you are on, and provides discovery metadata:
// PluginInfo.cs
using Vyshyvanka.Core.Interfaces;
[assembly: Plugin(
"vyshyvanka.plugin.httpclient",
Name = "Advanced HTTP Client",
Version = "1.0.0",
Description = "Advanced HTTP client nodes with retry, polling, and batch request capabilities.",
Author = "Vyshyvanka")]
If an assembly does not have this attribute, the loader skips it silently — only intentional Vyshyvanka plugins get registered.
Creating Custom Nodes
The power of the system lies in how we define nodes. A node class inherits from BasePluginNode and is decorated with attributes that describe its UI representation and configuration schema:
[NodeDefinition(
Name = "GraphQL Request",
Description = "Execute GraphQL queries and mutations against a GraphQL endpoint",
Icon = "fa-solid fa-diagram-project")]
[NodeInput("input", DisplayName = "Input")]
[NodeOutput("output", DisplayName = "Response", Type = PortType.Object)]
[ConfigurationProperty("endpoint", "string", Description = "GraphQL endpoint URL", IsRequired = true)]
[ConfigurationProperty("query", "string", Description = "GraphQL query or mutation string", IsRequired = true)]
[ConfigurationProperty("variables", "object", Description = "Query variables as key-value pairs")]
[ConfigurationProperty("headers", "object", Description = "Additional HTTP headers")]
[ConfigurationProperty("timeout", "number", Description = "Request timeout in seconds (default: 30)")]
public class GraphQLNode : BasePluginNode
{
public override string Type => "graphql-request";
public override NodeCategory Category => NodeCategory.Action;
public override async Task<NodeOutput> ExecuteAsync(NodeInput input, IExecutionContext context)
{
var endpoint = GetRequiredConfigValue<string>(input, "endpoint");
var query = GetRequiredConfigValue<string>(input, "query");
var variables = GetConfigValue<Dictionary<string, object>>(input, "variables");
var timeoutSeconds = GetConfigValue<int?>(input, "timeout") ?? 30;
// ... execute the GraphQL request ...
return SuccessOutput(responseData);
}
}
Notice that [ConfigurationProperty] attributes are placed on the class itself (not on individual properties). Configuration values are accessed at runtime through the GetConfigValue<T>() and GetRequiredConfigValue<T>() helper methods inherited from the base class. This design means the engine and Designer UI can discover the configuration schema via reflection without instantiating the node.
Plugin Validation
Before a plugin is registered, the PluginValidator checks it for correctness:
- Does it have the required
[Plugin]attribute? - Do its node types properly implement
INode? - Are there any dependency conflicts?
If validation fails, the plugin is rejected but the engine continues operating. You get a clear error in the logs telling you exactly what went wrong.
Package Management
Beyond dropping DLLs into a directory, Vyshyvanka has a full NuGet-based package management system. The NuGetPackageManager orchestrates the lifecycle:
-
PackageSearchService— Searches configured NuGet sources, resolves versions, checks for updates. -
PluginLoadingService— Loads plugin assemblies, validates them, and registers their node types with theNodeRegistry. -
NuGetPackageManager— Orchestrates install/update/uninstall using the above services.
This means you can install plugins from private NuGet feeds, manage versions centrally, and update plugins without restarting the engine.
Why This Design?
-
Strong Typing: By leveraging C# attributes, we automatically generate the UI schema for your nodes. When you add a new
[ConfigurationProperty], it appears in the Designer without writing frontend code. -
Isolation: Each plugin runs in its own
AssemblyLoadContext. If a plugin fails to load or crashes during execution, the rest of the engine remains operational. Plugin nodes are executed throughIPluginHostwith a timeout wrapper. -
Discoverability: The
NodeRegistryautomatically picks up allINodeimplementations from loaded plugin assemblies, making them available in the Designer immediately. - Hot Unload: Because load contexts are collectible, you can uninstall a plugin at runtime. The GC reclaims the memory once all references are released.
Exploring Real Examples
If you look into our ./plugins/ directory, you can see this in action:
-
Vyshyvanka.Plugin.AdvancedHttp— HTTP retry with exponential backoff, HTTP polling with success/failure conditions, batch requests with concurrency control, and GraphQL. -
Vyshyvanka.Plugin.GitLab— Full GitLab integration: issues, merge requests, pipelines, releases, files, tags, and webhook triggers. -
Vyshyvanka.Plugin.Jira— Jira issues, comments, search (JQL), users, and versions. -
Vyshyvanka.Plugin.Tmplt— A starter template for building your own plugin.
Getting Started with Your Own Plugin
- Create a new class library targeting the same .NET version.
- Add a reference to
Vyshyvanka.Core(the only allowed dependency from core). - Add your
PluginInfo.cswith[assembly: Plugin(...)]. - Create node classes inheriting
BasePluginNode(or implementINodedirectly). - Decorate with
[NodeDefinition]and[ConfigurationProperty]attributes. - Build, and either drop the DLL into the plugins directory or publish as a NuGet package.
Building for extensibility ensures that as your business needs evolve, your automation platform evolves with you.
In the next part, we will discuss Part 11: Testing Strategies - How to guarantee your workflows are rock-solid. Stay tuned!
Check out the project source code here: https://github.com/homolibere/Vyshyvanka
Top comments (0)