Selegic CRM Docs
ServerFlows

Execution Engine

Graph traversal, execution loop, safety guardrails, and concurrency model.

FlowExecutionEngine

Located in flow-execution.engine.ts, the engine uses Effect-TS to manage concurrency and error propagation.

Execution Loop

success failure retryable failed more nodes done Initialize Context Create FlowRun record Queue Start node Load node definition Create FlowExecutionNode Execute node logic Resolve outgoing edges Handle error/retry Queue next nodes Add to dead-letter queue Mark run complete/failed

Steps

  1. Initialize Context: Prepare ExecutionContext with trigger event data
  2. Persistence: Create FlowRun record in database
  3. Queue Nodes: Start with Start node
  4. Traversal:
    • Load current node and definition
    • Create FlowExecutionNode (status: PENDING)
    • Execute node logic (status: SUCCESS or FAILED)
    • Resolve outgoing edges based on result
    • Queue next nodes
  5. Finalize: Mark FlowRun as COMPLETED or FAILED

Safety Guardrails

The engine enforces limits in definitions.ts:

LimitDefaultDescription
maxSteps5000Total nodes visited in single execution
maxNodeVisits50Times a single node can be visited
parallelism6Concurrent branches in Parallel nodes
timeout300000msMax time for single node execution

Loop Detection

The maxNodeVisits limit prevents infinite loops:

if (nodeVisitCount > limits.maxNodeVisits) {
  throw new FlowError("MAX_NODE_VISITS_EXCEEDED");
}

Concurrency Model

The engine uses Effect-TS concurrency:

// Parallel nodes use unbounded concurrency, limited by config
Effect.all(nodes, { concurrency: "unbounded" })
  .pipe(Effect.withConcurrencyLimit(config.parallelism));

Parallel Execution

Nodes marked as parallel execute concurrently:

{
  "type": "parallel",
  "branches": [
    { "id": "branch1", "nodes": [...] },
    { "id": "branch2", "nodes": [...] }
  ]
}

Error Handling

Retry Strategy

interface RetryConfig {
  maxAttempts: number;
  delayMs: number;
  backoff: "linear" | "exponential";
}

Dead Letter Queue

Failed executions not retryable go to dead-letter:

await FlowDeadLetter.create({
  flowId,
  executionId,
  error: errorMessage,
  nodeId,
});

State Transitions

PENDING → RUNNING → SUCCESS
                  → FAILED
                  → RETRYING
                  
PENDING → RUNNING → DEAD_LETTER (after max retries)

On this page