Documentation

What Is a Thread?

A ProcessThread is the execution unit directly below a Process. It represents one distinct logical path — for example, "the approval path" or "the notification path". A process can have multiple threads; they run sequentially (one after the other, not concurrently at the process level).

Each thread has a version. The version contains the actual node graph. When a thread is updated in the workflow editor, a new version is created so in-flight executions can finish on the old version.

The thread owns an ExecutionMemory object — a mutable shared bag that all nodes in the thread read from and write to. Nodes do not communicate directly; they communicate through memory.

Thread Execution Loop

flowchart TD A(["ExecuteProcessThreadAsync"]) B["Initialize context\nMint ExecutionID · Stamp start time"] C["Load thread definition\nElements · Connections · Satellite nodes"] D["Gather satellite nodes\nAttach to host elements · Strip from main list"] E["Seed execution stack\nPush trigger nodes"] F["Register in active executions\nCreate ExecutionTrace"] G{"Stack empty?"} H["Check cancellation & pause signal"] I["Pop next element\nCreate ProcessElementExecutionContext"] J["ExecuteProcessElementAsync\nElement layer → Node executor"] K["Check abort signal"] L["Route output port\nPush downstream nodes"] M["Persist thread execution record\nDatabase audit trail"] N["Complete trace · Fire WorkflowCompleted event"] O(["Thread result"]) A-->B-->C-->D-->E-->F-->G G-- No -->H-->I-->J-->K-->L-->G G-- Yes -->M-->N-->O style A fill:#162040,stroke:#6c8cff,color:#c8d8ff style O fill:#162d22,stroke:#34d399,color:#c8ffd8 style G fill:#2d2616,stroke:#fbbf24,color:#e2e8f0 style K fill:#2d2616,stroke:#fbbf24,color:#e2e8f0

Step-by-Step

1

Initialize the execution context

A new ExecutionID (a GUID) is minted for this thread run. The ExecutorID links back to the parent ProcessID, AppID, and ProcessThreadID. StartedAtUtc is stamped and state is set to Running.

2

Load the thread definition

The full ProcessThreadDefinition is loaded — containing all element definitions, their connection edges, connector bindings, and satellite node trees. If already present in the operating context (resume path), the cached definition is reused.

3

Gather and attach satellite nodes

Satellite nodes are helper sub-nodes that belong to a host flow node (e.g., data-transform sub-nodes attached to an HTTP request node). ISatelliteNodeGatherer.GatherAndAttachAsync fetches them, attaches them to their host elements, and strips them from the main element list so the execution stack only contains flow nodes.

4

Seed the execution stack with trigger nodes

The ProcessElementExecutionStack is initialized. Trigger nodes — the entry points of the thread — are pushed first. If the execution context has a WebhookFilter.StartFromProcessElementID, only that single node is pushed instead of all trigger nodes.

5

Register in active executions & start tracing

The thread's ResID is registered in the in-memory _activeExecutions dictionary so external callers (pause/resume API) can locate this execution by ID. A ExecutionTrace is created; every node that executes adds a NodeExecutionTrace record.

6

The execution loop

The core is a while (stack.Items.Count > 0) loop. Each iteration: check cancellation → check pause signal → pop next element → call ExecuteProcessElementAsync → check abort signal → route output port → push downstream nodes.

7

Persist thread execution record

When the loop exits, a ProcessThreadExecution entity is created with timing, status, input/output JSON, and node counts, then inserted into the database.

8

Complete the trace & fire events

The ExecutionTrace is marked complete. The WorkflowCompleted event fires. Cleanup removes the thread from _activeExecutions (unless paused — paused executions stay registered to support resume).

Execution Memory

Every thread carries an ExecutionMemory instance from the first node to the last.

PropertyTypeMutable?Purpose
InputDataIReadOnlyDictionaryNoThe original trigger data. Never modified after creation.
VariablesDictionaryYesNamed values set and read by nodes. Scoped by the ScopeStack.
NodeOutputsDictionaryYesPer-node output data, keyed by ProcessElementKey. Downstream nodes read from here.
CacheDictionaryYesTransient scratch space. Not serialized on pause.
LoopStackStackYesTracks active loop nodes and their iteration counts.
ExceptionContextStackStackYesTracks active try-catch-finally scopes.
ScopeStackStackYesVariable scoping — global scope at bottom, function/loop scopes above.

Parallel Execution

Parallelism within a thread is handled by the parallel fork/join pattern:

  1. A fork node creates multiple execution lanes. Memory.IsParallelExecutionActive is set to true.
  2. While in parallel mode, each iteration of the main loop pulls from the active lane instead of the main stack.
  3. A join node waits until all lanes complete, merges their outputs into memory, and resumes normal sequential execution.
Cooperative, not preemptive Nodes in different lanes still execute one at a time within the same thread (same async context). True CPU parallelism requires separate threads or a separate orchestrator invocation.

Error Handling Within a Thread

If a node throws an unhandled exception, the thread checks whether the current execution position is inside a try-catch-finally scope (tracked on ExceptionContextStack).