In our last deep dive, we looked at how Vyshyvanka is put together under the hood. Today, we're zooming in on what actually makes the engine tick: the Domain Model. At the end of the day, Vyshyvanka isn't just a collection of services; it's a system designed to manage a directed graph of operations that we call a Workflow.
The Workflow: A Directed Graph
At its heart, a workflow is a directed graph where nodes represent operations and connections represent the flow of data.
- Exactly One Trigger: Every workflow needs a starting point. Whether it's a webhook, a timer, or a manual button press, we enforce that there is exactly one trigger node.
- Ownership: Every workflow is owned by a single user, ensuring that permissions and access are managed consistently across the board.
Nodes: The Atomic Operations
Nodes are the building blocks of your automation. We categorize them based on their behavior, which dictates their base class in our engine:
- Trigger Nodes: These start the workflow. They have no incoming connections because they define the entry point. Examples include our WebhookTrigger or ScheduleTrigger.
- Action Nodes: These do the actual work. Think of them as your worker bees - sending an email, querying a database, or calling an external HTTP API.
- Logic Nodes: These are the brains. They allow you to branch execution (If/Switch), create loops, or merge data streams.
When you implement a new node in Vyshyvanka, you simply inherit from the appropriate base class, override the ExecuteAsync method, and register it in the NodeRegistry. The engine takes care of the rest.
Connections and Type Safety
One of the most common sources of bugs in automation platforms is passing incompatible data between steps. We fixed that with a robust Port Type system.
A connection is valid if:
- The source port type equals the target port type.
- Either port is an 'Any' type, which acts as a wildcard.
Our supported types include Object, Array, String, Number, and Boolean. By enforcing this at the designer level, we ensure that by the time a workflow hits the engine, it's guaranteed to be syntactically and logically sound.
Execution: The Immutable Record
When a workflow runs, we create an Execution record. This is an immutable snapshot of that specific run. Why immutable? Because transparency matters. Once an execution is marked as 'Completed', 'Failed', or 'Cancelled', it is final. You can trace back exactly what data was passed to each node, what the result was, and how the state machine transitioned.
This transition logic is rigid:
Pending -> Running -> (Completed | Failed | Cancelled)
Terminal states are final, and that is a promise the engine keeps. This prevents any accidental state mutations or 'zombie' executions that haunt other automation systems.
Validation Rules
Before a workflow can even be executed, it must pass a battery of checks:
- No circular dependencies.
- Every node must be reachable from the trigger.
- Required properties must be set.
- All connections must satisfy port compatibility.
This pre-execution validation means that when you hit 'Run', you can be confident that the workflow isn't going to fail because of a simple wiring mistake.
Why This Matters
By building a strict, type-safe domain model, we've turned what is usually a fragile scripting process into a robust software engineering task. You aren't just "writing a script"; you are defining a formal model that the system understands and can verify.
In the next part, we'll look at the front end of this equation: The Designer. We'll explain how we built a Blazor WebAssembly canvas that allows you to drag, drop, and wire up these nodes in real time. Stay tuned!
Check out the project source code here: https://github.com/homolibere/Vyshyvanka
Top comments (0)