Workflow & Run Management
Flyte manages the lifecycle of workflow runs through a combination of gRPC services, persistent storage in PostgreSQL, and background reconciliation. The system tracks every execution from the initial trigger to final termination, maintaining a detailed history of state transitions and cluster events.
Workflow Run Lifecycle
When you trigger a workflow, Flyte initiates a run that progresses through several phases (e.g., QUEUED, RUNNING, SUCCEEDED, FAILED). The primary entry point for these operations is the RunService located in runs/service/run_service.go.
Creating a Run
To start a new execution, you call CreateRun. Internally, RunService performs the following steps:
- ID Generation: Generates a unique
RunIdentifierusing the project, domain, and a timestamp-based name. - Persistence: Calls
persistRunModelto save the run metadata and the initialActionSpecto the database via theActionRepo. - Execution Enqueue: Submits the run to the execution engine by calling
s.actionsClient.Enqueue.
// From runs/service/run_service.go
func (s *RunService) CreateRun(
ctx context.Context,
req *connect.Request[workflow.CreateRunRequest],
) (*connect.Response[workflow.CreateRunResponse], error) {
// ... validation and ID generation ...
runId = &common.RunIdentifier{
Project: id.ProjectId.Name,
Domain: id.ProjectId.Domain,
Name: generateRunName(time.Now().UnixNano()),
}
// ... storage and DB persistence ...
run, err := s.persistRunModel(ctx, runId, taskID, taskSpec, inputPrefix, runOutputBase, runSpec, request.GetSource(), triggerName, triggerTaskName, triggerRevision, triggerType)
// Enqueue to execution engine
_, err = s.actionsClient.Enqueue(ctx, connect.NewRequest(&actions.EnqueueRequest{
Action: &actions.Action{
ActionId: actionID,
InputUri: inputPrefix,
RunOutputBase: runOutputBase,
Spec: &actions.Action_Task{
Task: &workflow.TaskAction{
Id: taskID,
Spec: taskSpec,
CacheKey: wrapperspb.String(cacheKey),
},
},
},
RunSpec: runSpec,
}))
// ...
}
State Persistence and the Root Action
Flyte represents the top-level workflow run as a special action named a0. All subsequent tasks or sub-workflows within that run are stored as child actions in the actions table. The ActionRepo (implemented in runs/repository/impl/action.go) handles the CRUD operations for these entities.
When an action's phase changes, UpdateActionPhase updates the database and triggers a notification:
- If the root action
a0is updated, it notifies subscribers of a Run update. - For any action, it notifies subscribers of an Action update.
Reliable Termination
If you need to stop a running workflow, you call AbortRun. However, simply marking a run as ABORTED in the database does not immediately stop the underlying Kubernetes pods or external resources.
The Abort Reconciler
Flyte uses the AbortReconciler (found in runs/service/abort_reconciler.go) to ensure that abort requests are reliably propagated to the execution engine.
AbortRunmarks the root actiona0asABORTEDin the database.- The
AbortReconcilerbackground service polls for actions in a pending abort state. - It uses a deduplicated work queue and exponential backoff to retry termination calls (
actionsClient.Abort) until the execution engine confirms the action has stopped.
You can configure the reconciler's behavior via AbortReconcilerConfig:
Workers: Number of concurrent termination goroutines.MaxAttempts: Maximum retries per action before giving up.QueueSize: Buffer size for the internal work channel.
Real-time State Tracking
Flyte provides streaming APIs (WatchRuns and WatchActions) to allow frontends and CLI tools to track execution progress in real-time without polling.
LISTEN/NOTIFY Mechanism
The ActionRepo utilizes PostgreSQL's LISTEN and NOTIFY commands to broadcast updates. When UpdateActionPhase is called, it executes a NOTIFY on a specific channel. The Watch methods in RunService subscribe to these channels to receive immediate updates.
Tree-based State Management
For complex workflows with many nested tasks, Flyte uses an internal runStateManager (runs/service/run_state_manager.go) to maintain a tree-based view of the run. This manager:
- Aggregates Phase Counts: Tracks how many children are in each phase (e.g., 5 succeeded, 2 running) to provide summary statistics for parent nodes.
- Handles Filtering: Efficiently determines which nodes should be visible based on user-provided filters (e.g., "show only failed tasks").
- Maintains Visibility: Ensures that if a child matches a filter, all its ancestors remain visible in the tree so the user can see the full path.
// From runs/service/run_state_manager.go
func (rsm *runStateManager) modifyPhaseCounters(current *node, toPhase, fromPhase common.ActionPhase, changed map[string]struct{}) {
for current != nil {
if toPhase != common.ActionPhase_ACTION_PHASE_UNSPECIFIED {
current.ChildPhaseCounts[toPhase]++
}
if fromPhase != common.ActionPhase_ACTION_PHASE_UNSPECIFIED {
count := current.ChildPhaseCounts[fromPhase]
current.ChildPhaseCounts[fromPhase] = max(0, count-1)
}
changed[current.Action.Name] = struct{}{}
current = current.Parent
}
}
Event History and Cluster Events
Granular execution details, such as Kubernetes pod events or container logs, are recorded as ActionEvents.
Internal Run Service
The InternalRunService (in runs/service/internal_run_service.go) provides the RecordActionEvent endpoint used by the execution engine to report progress. These events are stored in the action_events table and linked to specific action attempts.
Watching Cluster Events
You can stream these low-level events using WatchClusterEvents. This method fetches existing events from the database and then waits for new ones via the same ActionRepo notification mechanism used for phase updates. This allows you to see pod scheduling, image pulling, and container start/stop events as they happen.
// From runs/service/run_service.go
func (s *RunService) WatchClusterEvents(
ctx context.Context,
req *connect.Request[workflow.WatchClusterEventsRequest],
stream *connect.ServerStream[workflow.WatchClusterEventsResponse],
) error {
// ...
// Start watching first to reduce the chance of missing action updates
updatesCh := make(chan *models.Action, 50)
go s.repo.ActionRepo().WatchActionUpdates(ctx, actionID, updatesCh, errsCh)
// Drain all available cluster events for current action
info, err := s.getClusterEventsInfo(ctx, actionID, attempt, lastUpdatedAt, offset, maxEvents)
// ...
}
Eventual Consistency Gotcha
Note that event recording is eventually consistent. If you call GetRunDetails immediately after an event is triggered in the cluster, it may take a few moments for the InternalRunService to process the event and for it to appear in the API response.