Portal Community

Node Executor Lifecycle

  1. Flow Studio designer places a node on the canvas and wires its input/output ports
  2. Operator triggers the workflow (manually, on a schedule, or via an API event)
  3. Process engine resolves the node's executor type from the DI container
  4. Process engine calls ExecuteAsync(INodeExecutionContext context)
  5. Executor reads input data from context.Inputs, performs its work, writes to context.Outputs
  6. Process engine reads outputs and routes them to the next node in the graph
  7. 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 CaseReason Workflow Mode Fits
Multi-step approval processEach step is sequential; state carried in workflow context
ETL / data transformationInput → transform → output maps naturally to node ports
Scheduled report generationTriggered once per schedule; no need for a persistent service
Conditional branching logicGateway nodes route based on output port values
Human-in-the-loop tasksWorkflow suspends at HIL node; resumes on approval signal
Infrequent batch operationsNo value in keeping a server running 24/7 for rare executions

Workflow Mode Constraints

ConstraintDetail
No persistent state between executionsEach workflow run is independent; shared state goes in a database
Execution timeoutLong-running nodes (hours) must use suspend/resume — not blocking async
Single-caller per executionA workflow node instance is not shared; no concurrency within the node
DI scope is per-executionScoped services (DbContext) are fresh per run — correct behaviour
No streaming to callerWorkflow 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.