Build modern, AI-controllable workflow editors on any .NET GUI — WPF, Avalonia, WinUI, MAUI, WinForms, Razor, or Jalium.
📖 Wiki — Online (WASM) · Local — the online Wiki is a WebAssembly app, so its load speed depends on your network.
What this is — a node editor / node-graph / workflow-editor framework for .NET / C#. Drag nodes, wire slots into links on a zoomable, virtualized canvas, drive the graph with a compiled, pull-based execution engine, gate structural edits behind undo/redo, and control it all through an AI agent (function calling + MCP). One model → 7 GUIs: WPF · Avalonia · WinUI · MAUI · WinForms · Blazor · Jalium.
VeloxDev gives .NET developers a complete foundation for building interactive workflow editors — the kind where users drag nodes, wire slots together, and watch data flow through a graph at runtime.
Three ideas hold the whole project together:
- One model, every GUI. The workflow model, compile-time identity, runtime engine, serialization and the command/undo-redo stack live in
VeloxDev.Corewith zero UI dependencies. Platform adapters (WPF, Avalonia, WinUI, MAUI, WinForms, Razor, Jalium) supply the views plus the platform glue only they can provide — timers and frame pacers, thread marshalling, native value conversion. Your graph data and its execution semantics behave identically on every platform. - A real execution engine, not just a canvas. Besides the drag-and-drop surface,
CompilerExcompiles any reachable sub-graph into a plan (linear chain / router branch / fan-out group) and drives it deterministically — including reverse (Terminal) compilation: ask "what would this node output?" and it computes just the ancestor cone that feeds it, with no controller needed. Fan-out branches run concurrently as interleaved async operations, not as threads — every branch starts on the caller's context and eachawaitinside it yields to its siblings, so I/O-bound branches (a node invoking a process, say) overlap while the group stays on the host'sSynchronizationContext. A branch that burns CPU still occupies the thread in turn: a compiled run shares one runtime blackboard, which is deliberately not thread-safe. - AI is a first-class controller. A 60+ function-calling Workflow Agent lets an LLM inspect, build and mutate graphs at runtime through natural language — with the same undo/redo, validation and lifecycle the GUI uses, plus optional MCP tool connectivity.
Why not just a WPF node editor? Libraries like Nodify and NodeNetwork are excellent, but they are WPF-only canvases — they draw the graph and stop there. VeloxDev runs the same node graph on Avalonia, WinUI, MAUI, WinForms and Blazor, and layers on what a canvas alone cannot give you: a compiled forward + reverse execution engine, and an AI agent that edits and runs the graph through the same commands the GUI uses.
| Layer | What it provides | Dependency |
|---|---|---|
| ⛓️ Workflow | Tree / Node / Slot / Link templates with undo-redo, spatial indexing, deep-zoom canvas math, serialization, and a compiled execution engine (forward + reverse) | |
| 🤖 Workflow Agent | 60+ Function Calling tools — an AI can create nodes, wire slots, patch properties and run chains at runtime via natural language. Supports MCP (Model Context Protocol) for external tools/data. Ships bilingual (en/zh) embedded prompt docs so the agent knows exact tool semantics. | VeloxDev.Core.Extension |
| Layer | What it provides | Dependency |
|---|---|---|
| 🪶 MVVM | Source generators for observable properties and async, cancellable commands — keeps node ViewModels lightweight | |
| 🎞️ Transition | Cross-platform interpolation animation with easing & Fluent API — smooth visual feedback for workflow state changes | Platform Adapter Package |
| 🎨 Theme | Runtime theme switching with animated transitions — instant visual identity for your editor | Platform Adapter Package |
| 🌀 AOP | Generated aspect interfaces with runtime proxies — intercept node members for logging or validation without modifying business logic | |
| ⚙️ Tickable | Frame-driven lifecycle loop — tick-based node simulation or real-time graph execution |
| Platform | Package | NuGet |
|---|---|---|
| WPF | VeloxDev.WPF |
|
| Avalonia | VeloxDev.Avalonia |
|
| WinUI | VeloxDev.WinUI |
|
| MAUI | VeloxDev.MAUI |
|
| WinForms | VeloxDev.WinForms |
|
| Razor | VeloxDev.Razor |
|
| Jalium | VeloxDev.Jalium |
Adapter API docs: WinForms · Razor
- A workflow is a Tree of Nodes. Every Node is a ViewModel + a Helper (component/helper pattern). Nodes own Slots; slots are wired into Links. A slot has a channel (one/many, sender/receiver/both) that governs how connections are validated.
- Views are templates over the model. The adapter's
WorkflowSurfacehosts Node/Slot/Link views (plus grid decorator, minimap, ruler and tree views), virtualized against a spatial index so graphs with thousands of nodes stay fluid. - Structural edits are undoable commands. Create/delete node or slot, connect/disconnect, selector changes and their cascades all flow through
IVeloxCommandwith a redo/undo pair — and the Agent tool layer uses the same commands the GUI does. Position and size are outside that history:SetAnchor/SetSizeare commands with no undo entry, in the GUI and in the Agent alike. - Execution is compiled, then driven. Nodes expose one receive entry
ReceiveCommand → Helper.ReceiveAsync(ITaskContext, ct). The engine (RuntimeEngine) compiles a sub-graph into segments once and then pulls data through them — nodes do not broadcast to each other on their own during a compiled run. - Phases are explicit. Compile time assigns every node a fixed identity (
Order/ChainIndex/Offset) and runs dataflow validation without data; runtime reuses the sameAccessAsyncgate with real payloads. A context hierarchy (IContext → IAccessContext → ITaskContext, plusICompileContext/IRuntimeContext) describes every hand-off.
CompilerViewModel has one API, CompileAsync(node, role):
public enum CompileRole { Root, Terminal } // the node is a starter, or the result you want| Role | Meaning | What it compiles |
|---|---|---|
Root |
The node starts a run (e.g. a controller) | Its reachable sub-graph, walking downstream along Targets |
Terminal |
You want this node's result | Its ancestor cone — the producers feeding it, walked backward along Sources — starting automatically from the cone's entry frontier |
var compiler = new CompilerViewModel();
// 1) Forward: run the whole chain from a controller
var graph = (await compiler.CompileAsync(controller, CompileRole.Root))[0];
var session = new RuntimeContext();
await new RuntimeEngine().RunAsync(graph, session);
Console.WriteLine(session.Data); // the chain's final payload
// 2) Reverse: compute just one node's value — no start node needed
var cone = (await compiler.CompileAsync(someNode, CompileRole.Terminal))[0];
var probe = new RuntimeContext { Target = someNode };
await new RuntimeEngine().RunAsync(cone, probe);
if (probe.TargetReached) Console.WriteLine(probe.Data); // that node's output
else Console.WriteLine("target NOT reached — no value fabricated");The plan it produces is a small tree of segments: a linear chain, a router branch (a node implementing ICompileTimeRouter, Static or Dynamic), and fan-out groups (branches off the same source, driven concurrently as interleaved async operations) — and it is always acyclic. Looping is expressed as runtime redirects instead of graph cycles: when a node signals an error, the engine checks for IRedirectable and, if the node implements it, re-runs the graph toward the returned target under an internal retry limit. Redirects are therefore an extension point you implement on your own nodes — the engine supports them end to end, and the demo's Python node is the reference implementation (its script names the node to fall back to, by title, and the node resolves that to a compile order).
Three properties worth calling out, because they keep reverse compilation honest:
- Branches are real, never bypassed. Terminal compilation keeps a router's true
BranchSegmentbehavior; it only compiles the branch that leads into the target's cone. If the router actually selects a sibling branch at runtime, the target is not reached — you get an explicit "… was NOT reached … No result was produced." outcome, never a fabricated value. (Agent tools surface this as a specific error message; set the router's selection to the right branch and retry.) - Joins aggregate by source. A multi-input node receives an
IGroupData— a read-only map keyed by its upstream node — so a join "waits for all inputs" however the fan-out's branches interleaved (the shared runtime session is intentionally not thread-safe, which is why a branch that burns CPU still takes the thread in turn rather than running beside its siblings). TargetReachedmeans "the target was driven", not "a value was produced". The flag is set as the engine enters the target node, so read it together with the run status: a target that throws surfaces as a run error (Run failed: …), not as a reached target.
// Minimal "node" — the generator wires INotifyPropertyChanged, slot lifecycle and commands.
[WorkflowBuilder.Node<MyNodeHelper>]
public partial class MyNodeViewModel
{
public MyNodeViewModel() => InitializeWorkflow();
[AgentContext(AgentLanguages.English, "Input slot (receiver)")]
[VeloxProperty] public partial MySlotViewModel InputSlot { get; set; }
[AgentContext(AgentLanguages.English, "Output slot (sender)")]
[VeloxProperty] public partial MySlotViewModel OutputSlot { get; set; }
[VeloxProperty] private string title = "My Node";
}The Workflow Agent turns a workflow Tree into a tool surface for any IChatClient (Microsoft.Extensions.AI), so an LLM can inspect the graph, build nodes, connect slots, patch properties, and run — or reverse-compute — compiled chains:
var scope = tree.AsAgentScope()
.WithAutoDiscovery(assemblyName: "MyApp")
.WithInteractionSafety(3) // confirm before destructive ops; present choices via tool
.WithSelectionHandler(ShowDialog)
.WithConfirmationHandler(ShowDialog);
var agent = chatClient.AsAIAgent(
instructions: scope.ProvideProgressiveContextPrompt(),
tools: scope.ProvideTools());Highlights of the tool surface:
- Inspect & mutate like the GUI —
ListNodes,GetFullTopology,CreateNode,ConnectByProperty,PatchNodeProperties,SetEnumSlotCollection,Undo/Redo,MoveNode, … every mutation dispatches the same component command the GUI dispatches, so the Agent and the GUI share one edit path — including the same undo semantics, i.e. the moves and property patches that create no undo entry. - Execute at three levels — node-level (
ExecuteNode), chain-level (RunCompiledWorkflow, Root role), and result-level (GetNodeResult, Terminal role). Plans can be read without running viaCompileWorkflow/CompileNodeResult. - Gated by policy, not just prose — node-execution tools are disabled until the host calls
WithAllowNodeExecution(true); generic command execution is allow-listed; interaction tools appear only when a selection/confirmation handler is wired.MaxToolCalls,MaxReadToolCallsandMaxWriteToolCallsbound a session. - Precision is baked into the prompt, in the host's language — embedded (en/zh) prompt docs describe tool semantics, error/rejection handling, mount-before-operate and the exact "target not reached" contract, so the agent knows before calling what each tool does and what errors mean.
var mcp = new McpScope()
.WithMcpRoot(".evn/mcp")
.WithSynchronizationContext(SynchronizationContext.Current);
var configs = new[]
{
// Local stdio server (npx)
new McpServerConfiguration
{
Name = "Filesystem",
RunMode = McpServerRunMode.Npx,
Package = "@modelcontextprotocol/server-filesystem",
Arguments = ["C:/data"],
},
// Remote server over Streamable HTTP (SSE fallback for legacy servers)
new McpServerConfiguration
{
Name = "Microsoft Learn",
RunMode = McpServerRunMode.Http,
Endpoint = "https://learn.microsoft.com/api/mcp",
Options = new { connectionTimeout = 30 },
// Header auth: Options = new { headers = new { Authorization = "Bearer <token>" } }
// OAuth 2.0: Options = new { oauth = new { clientId = "...", redirectUri = "...", scopes = new[] { "read" } } }
},
};
var mcpTools = await mcp.LoadAsync(configs);
var allTools = scope.ProvideTools().Concat(mcpTools).ToArray(); // merge into the agentMcpScope installs npm packages idempotently, manages stdio/HTTP transports, reports per-server failures without blocking the rest, and supports OAuth via WithOAuthAuthorizationRedirect(...).
Install one of the platform adapter packages listed above and you get everything — workflow, execution engine, agent, animations and theming — wired up for that GUI.
Each adapter ships a dotnet new template pack that generates the full view suite — Node, Slot, Link, Tree, template selector, grid decorator and minimap — pre-wired to the model. WPF example (replace MyApp with your root namespace):
dotnet new install VeloxDev.WPF.Templates
dotnet add package VeloxDev.WPF
dotnet new wpf-v-slot -n SlotView -ns MyApp.Views -o Views
dotnet new wpf-v-node -n NodeView -ns MyApp.Views -o Views
dotnet new wpf-v-link -n LinkView -ns MyApp.Views -o Views
dotnet new wpf-v-selector -n TemplateSelector -ns MyApp.Views -o Views
dotnet new wpf-v-decorator -n GridDecorator -ns MyApp.Views -o Views
dotnet new wpf-v-minimap -n MinimapOverlay -ns MyApp.Views -o Views
dotnet new wpf-v-tree -n TreeView -ns MyApp.Views -o Views
dotnet buildAvalonia, WinUI, MAUI, WinForms and Jalium suites expose the same style options (jalium-v-* for Jalium). Common style aliases:
| Template | Style aliases |
|---|---|
| Node | -bg background, -fg foreground, -bb border brush, -bt border thickness, -cr corner radius |
| Slot | -bg background, -sc standby color, -bc border color, -sp SVG path data |
| Link | -lc line color, -lt line thickness |
| Tree | -bg background, -bb border brush, -bt border thickness, -cr corner radius |
| Grid decorator | -bg background, -mic minor color, -mac major color, -ac axis color, -gs spacing, -mle major interval, -rb ruler background, -rtc ruler tick color, -rlc ruler label color, -rdc ruler divider color |
| Minimap overlay | -bg background, -bdr border, -nf node fill, -vs viewport stroke |
All templates use -ns for the generated namespace.
VeloxDev/
├── VeloxDev.slnx
├── Src/
│ ├── Core/
│ │ ├── VeloxDev.Core # Workflow model, generators, canvas math, CompilerEx engine
│ │ ├── VeloxDev.Core.Extension # Workflow Agent tools, MCP scope, runtime extensions
│ │ ├── VeloxDev.Core.Test # Unit tests (model / canvas / execution)
│ │ └── VeloxDev.Core.Extension.Test # Unit tests (agent tools / serialization / lifecycle)
│ ├── Adapters/
│ │ ├── VeloxDev.WPF · VeloxDev.Avalonia · VeloxDev.WinUI · VeloxDev.MAUI
│ │ ├── VeloxDev.WinForms · VeloxDev.Razor · VeloxDev.Jalium # one view layer per platform
│ ├── Generators/
│ │ └── VeloxDev.Core.Generator # Roslyn source generators (netstandard2.0)
│ └── Templates/ # dotnet new item template packs per GUI adapter
├── Examples/
│ ├── Workflow/ # WPF · Avalonia · WinUI · WinForms · MAUI · Razor · Jalium
│ │ # (+ "Trimmed" variants that compile no extraneous generator code)
│ │ # Common/Lib holds the shared demo node library (Controller, routers, python workers)
│ ├── MVVM/ # WPF · Avalonia
│ ├── Transition/ # WPF · Avalonia · WinUI · WinForms · MAUI · Razor · Jalium
│ ├── Theme/ · AOP/ # WPF · Avalonia
│ └── Tickable/ # WPF
└── Docs/
└── VeloxDev.Docs # Documentation site (WebAssembly: online/local wiki)
Two test suites live beside the source (VeloxDev.Core.Test, VeloxDev.Core.Extension.Test) and are deliberately fast (~500+ assertions with no I/O in the hot path):
dotnet test # or open VeloxDev.slnx and run in Visual Studio
# targeted:
dotnet test Src/Core/VeloxDev.Core.Test --filter "FullyQualifiedName~CompilerEx"Coverage is collected with coverlet (--collect:"XPlat Code Coverage"). The CompilerEx execution engine — compile decomposition, runtime driving, redirects, joins, and reverse compilation — is covered end-to-end with self-contained contract tests (probe nodes, no UI/no demo dependency). A full XML-comment/language pass keeps the public API documented in English.
Worth knowing before you depend on them:
- Not AOT- or trim-safe, and it says so.
VeloxDev.CoredeclaresIsTrimmable=false, and the animation path compiles expression trees at runtime (Expression.Lambda(...).Compile()). There are noRequiresUnreferencedCode/DynamicallyAccessedMembersannotations anywhere in it. (TheExamples/*/"Trimmed"folders are minimal demos — the name is not about trimming configuration.) - Redirects are a contract, and one node ships with it. The engine drives
IRedirectableend to end; the demo's Python node is the reference implementation (its script names the node to fall back to and the node resolves that name to a compile order), and the demo graph uses it to send a run back when the data is too thin to trust. A redirect target must still be strictly backward, and a target inside a nested branch is out of reach — see the limitation noted with the demo graph. - Undo coverage is structural. Node/slot create and delete, connect/disconnect, selector changes and their cascades are undoable; position, size and direct property patches are not.
- Fan-out overlaps, but it is not thread parallelism — branches are interleaved async operations on the host's
SynchronizationContext(see above). A compiled run shares one runtime blackboard, which is intentionally not thread-safe, so a node that starts its ownTask.Runis on its own. - Waiting, sleeping and offloading CPU work are node-body decisions, not engine features.
ReceiveAsyncis async, so a timer isawait Task.Delay(...)inside the node, an external signal is awaiting aTaskCompletionSource, and real parallelism for one branch isTask.Run— subject to the caveat above about not touching the shared session from that thread. Timing and threading therefore look absent from the engine's feature list rather than missing from it. What does not follow from this is a wait that outlives the process: a run in flight lives in memory, so a durable wait rides on the checkpoint — save the place, reload the graph, carry on — andExecutionCheckpoint.Rekeyis what makes the reload legal, since a restored graph has fresh node identities. - A run can be paused, watched, retried, checkpointed and resumed — off by default.
RuntimeContexttakes anIExecutionGate, anIExecutionObserver, anINodeRetryPolicy, anIExecutionErrorSink, anIExecutionCompensationand anIExecutionCheckpointStore; with none configured a run behaves exactly as it did before they existed, down to the log lines and the number of node drives. What still has no shipped implementation isIRedirectable(a node's own fallback contract) — see the note above. - Animation overshoot is per-type today. When an ease overshoots (Back/Elastic), numeric samplers extrapolate past the endpoint while the remaining samplers pin to it. Unifying the two is an open pass.
- The one model / 7 GUIs seam is at data and geometry, not rendering. The transition engine and the workflow surface math live in Core and each adapter contributes only a small pacer subclass, but the view layer (surface behaviour, virtualization window, pooling, link rendering) is implemented per platform.
Released under the MIT License. © 2025 Axvser