Octopus — Workflow
Workflow Mode
In workflow mode, an execution node is a step inside a Flow Studio workflow. The process engine creates a node executor instance, passes it the execution context, and calls ExecuteAsync. When the method returns, the node is done and the next step begins.
Node Executor Lifecycle
- Flow Studio designer places a node on the canvas and wires its input/output ports
- Operator triggers the workflow (manually, on a schedule, or via an API event)
- Process engine resolves the node's executor type from the DI container
- Process engine calls
ExecuteAsync(INodeExecutionContext context) - Executor reads input data from
context.Inputs, performs its work, writes tocontext.Outputs - Process engine reads outputs and routes them to the next node in the graph
- Executor is released (disposed if IDisposable)
BaseNodeExecutor Pattern
public abstract class BaseNodeExecutor : INodeExecutor
{
// Implement in your subclass
public abstract Task ExecuteAsync(INodeExecutionContext context,
CancellationToken cancellationToken);
// Helpers available to all executors
protected T GetInput<T>(INodeExecutionContext ctx, string portName)
=> ctx.Inputs.Get<T>(portName);
protected void SetOutput<T>(INodeExecutionContext ctx, string portName, T value)
=> ctx.Outputs.Set(portName, value);
protected ILogger Logger => ctx.Logger;
}
// Example: Invoice approval node
public class InvoiceApprovalNodeExecutor : BaseNodeExecutor
{
private readonly IInvoiceService _invoices;
private readonly IEmailService _email;
public InvoiceApprovalNodeExecutor(IInvoiceService invoices,
IEmailService email)
{
_invoices = invoices;
_email = email;
}
public override async Task ExecuteAsync(
INodeExecutionContext context,
CancellationToken cancellationToken)
{
var invoiceId = GetInput<int>(context, "InvoiceId");
var approver = GetInput<string>(context, "ApproverEmail");
var invoice = await _invoices.GetAsync(invoiceId, cancellationToken);
// Send approval request email
await _email.SendApprovalRequestAsync(approver, invoice, cancellationToken);
// Write decision port — downstream gateway reads this
SetOutput(context, "ApprovalRequestSent", true);
SetOutput(context, "InvoiceAmount", invoice.TotalAmount);
}
}
Input / Output Port Wiring
Ports are declared in the node's JSON descriptor and wired in Flow Studio. The process engine validates that required input ports are connected before the workflow starts.
// Node descriptor JSON (registered in the node catalogue)
{
"nodeType": "InvoiceApprovalNode",
"displayName": "Invoice Approval Request",
"category": "Finance",
"inputPorts": [
{ "name": "InvoiceId", "type": "integer", "required": true },
{ "name": "ApproverEmail", "type": "string", "required": true }
],
"outputPorts": [
{ "name": "ApprovalRequestSent", "type": "boolean" },
{ "name": "InvoiceAmount", "type": "decimal" }
]
}
When to Use Workflow Mode
| Use Case | Reason Workflow Mode Fits |
|---|---|
| Multi-step approval process | Each step is sequential; state carried in workflow context |
| ETL / data transformation | Input → transform → output maps naturally to node ports |
| Scheduled report generation | Triggered once per schedule; no need for a persistent service |
| Conditional branching logic | Gateway nodes route based on output port values |
| Human-in-the-loop tasks | Workflow suspends at HIL node; resumes on approval signal |
| Infrequent batch operations | No value in keeping a server running 24/7 for rare executions |
Workflow Mode Constraints
| Constraint | Detail |
|---|---|
| No persistent state between executions | Each workflow run is independent; shared state goes in a database |
| Execution timeout | Long-running nodes (hours) must use suspend/resume — not blocking async |
| Single-caller per execution | A workflow node instance is not shared; no concurrency within the node |
| DI scope is per-execution | Scoped services (DbContext) are fresh per run — correct behaviour |
| No streaming to caller | Workflow output is delivered at completion, not incrementally |
Long-running nodes. If a node performs work that takes more than a few minutes (e.g. waiting for an external event), implement the suspend/resume pattern using the HIL actor mechanism rather than blocking
await inside ExecuteAsync.