Every diagram you draw with Beep.Skia can also be executed. The workflow engine walks the connected components in dependency order and runs each one as an automation node, passing data along the connections. This is what turns a diagram from documentation into a runnable process.

Execution Model

Components that implement IAutomationNode are the units of work. The engine sorts them topologically from the diagram's connections, so a node runs only after everything feeding into it has completed. Each execution gets its own ExecutionContext carrying input data, variables, and the execution id.

ConceptTypePurpose
EngineWorkflowEngine (implements IWorkflowEngine)Loads workflow definitions and executes them
DefinitionWorkflowDefinitionNodes, connections, variables, retry policies, priority
NodeIAutomationNodeInitialize / validate / execute, with status and error events
ContextExecutionContextPer-execution data, variables, and node results
ResultWorkflowResultFinal status, per-node results, output data, errors

Built-in Node Types

The engine registers these by default, so a diagram built from them runs without any wiring:

NodeRole
ManualTriggerNodeStarts a run on demand (button text, shortcut, cooldown, auto-reset)
TimerTriggerNodeStarts a run on a schedule or interval
DataInputNodeFeeds a dataset into the pipeline
DataTransformNodeMap / filter / aggregate / custom transforms with field mappings
ConditionalNodeBranches on an expression, with true/false outputs
DataSourceAutomationNodeReads or writes through a registered data source
HttpRequestNodeCalls an HTTP endpoint (method, URL, headers, body)

Custom nodes are registered by type name, which is also how diagram-sourced workflows resolve their components:

Registering a custom node

var engine = new WorkflowEngine();
engine.RegisterNodeType("MyCustomNode", () => new MyCustomNode());
engine.RegisterNodeType(typeof(MyCustomNode).FullName, () => new MyCustomNode());

Inputs and Variables

Workflow variables are seeded into every node's context before execution, and input data supplied at run time is available to all nodes. This is how the same workflow behaves differently per invocation — an environment name, a batch size, a date range.

Variables and input data

var workflow = new WorkflowDefinition("nightly", "Nightly Sync");
workflow.Variables.Add(new WorkflowVariable("environment", DataType.String)
{
    DefaultValue = "production",
    IsRequired = true
});

await engine.LoadWorkflowAsync(workflow);
var result = await engine.ExecuteWorkflowAsync("nightly", new Dictionary<string, object>
{
    ["batchSize"] = 500
});

Retries and Timeouts

Reliability is configured per node, not globally, so a flaky HTTP call can retry while a deterministic transform fails fast:

Retry policies

nodeDef.RetryPolicy = RetryPolicy.Simple(maxRetries: 3, delaySeconds: 2);
nodeDef.RetryPolicy = RetryPolicy.ExponentialBackoff(maxRetries: 5, initialDelaySeconds: 1, maxDelaySeconds: 30);
nodeDef.ExecutionTimeout = TimeSpan.FromSeconds(30);

A node that keeps failing marks the execution Failed and the error is recorded against the node id, so you can see exactly where a run broke.

Pause, Resume, Cancel

Long-running workflows can be controlled while they execute. Pausing takes effect at the next node boundary, and cancelling propagates a cancellation token into the running node:

Controlling a running execution

await engine.PauseExecutionAsync(executionId);   // holds before the next node
await engine.ResumeExecutionAsync(executionId);
await engine.CancelExecutionAsync(executionId);  // cooperative cancellation

Results and Events

The engine raises events for the whole lifecycle — WorkflowStarted, WorkflowCompleted, WorkflowFailed, WorkflowPaused, WorkflowResumed, WorkflowCancelled — and returns a WorkflowResult with per-node results and the accumulated output data. Execution history is kept per workflow and is safe to query from other threads while runs are in flight.

Running a Diagram

DrawingManager.ToWorkflowDefinition() converts the current canvas into a workflow: automation components become nodes (with their configuration), and connection lines become the dependency graph.

Diagram to execution

var manager = new DrawingManager();
manager.AddComponent(new ManualTriggerNode { X = 40, Y = 40, Name = "Start" });
manager.AddComponent(new DataTransformNode { X = 40, Y = 180, Name = "Transform" });
manager.ConnectComponents(manager.GetComponents()[0], manager.GetComponents()[1]);

var workflow = manager.ToWorkflowDefinition("Saved Diagram");
await engine.LoadWorkflowAsync(workflow);
var result = await engine.ExecuteWorkflowAsync(workflow.Id);
Saved diagrams keep their wiring and configuration. Port identities and each automation node's runtime configuration (transform mappings, filters, and so on) survive a save/load round-trip, so a workflow that ran yesterday still runs after being reopened. This is covered by dedicated regression tests.

Related