Skip to main content

Understanding Runs and Actions

Flyte manages workflow executions through a unified model where the distinction between a high-level "Run" and an individual "Action" is purely structural. By treating every step of a workflow as an action, Flyte can represent complex, nested execution hierarchies using a single database entity.

The Unified Action Model

In Flyte, every execution unit is an Action. Whether you are running a single task, a sub-workflow, or a complex conditional branch, the underlying data structure remains the same.

The Action struct (defined in runs/repository/models/action.go) contains the metadata required to track an execution:

type Action struct {
Project string `db:"project"`
Domain string `db:"domain"`
RunName string `db:"run_name"`
Name string `db:"name"`

// Parent action (NULL for root actions/runs)
ParentActionName sql.NullString `db:"parent_action_name"`

// High-level status (e.g. 1 = QUEUED)
Phase int32 `db:"phase"`

// ... other metadata fields ...
}

Runs vs. Actions

A Run is simply the root of an execution tree. In the codebase, this is explicitly defined as a type alias:

// Run is a type alias for Action (runs are just actions with ParentActionName == nil)
type Run = Action

When you initiate a workflow, Flyte creates a root action. This root action has no ParentActionName, marking it as the entry point for the entire execution.

Creating a Run and the "a0" Convention

When you call CreateRun via the RunService, Flyte initializes the execution by creating a root action. By convention, this root action is always named a0.

You can see this initialization in runs/service/run_service.go:

// From RunService.CreateRun
actionID := &common.ActionIdentifier{
Run: runId,
Name: RootActionName, // RootActionName is defined as "a0"
}

This a0 action serves as the parent for all subsequent steps in the workflow. Even if your workflow only contains a single task, that task will be recorded as a child of a0.

Building the Execution Hierarchy

As a workflow progresses, the executor or external systems record individual steps using RecordAction. This is where the hierarchy is built. Each child action specifies its parent via the ParentActionName field.

For example, if a workflow (a0) triggers a task, that task might be recorded with:

  • RunName: The name of the original run.
  • Name: A unique identifier for the task (e.g., a1).
  • ParentActionName: a0.

This structure allows Flyte to support deeply nested executions, such as workflows-within-workflows, where a child action can itself be a parent to further actions.

Internal State Management

To provide real-time updates and aggregate status information (like how many tasks in a workflow have succeeded), Flyte uses an internal tree representation.

The node struct in runs/service/run_state_manager.go is used by the runStateManager to build this tree in memory:

type node struct {
Parent *node
Action *models.Action
Children []*node

ChildPhaseCounts map[common.ActionPhase]int
MatchingDescendantCount int
}

Phase Aggregation

The node structure allows Flyte to calculate aggregate states efficiently. The ChildPhaseCounts map tracks the status of all immediate children. This is critical for features like WatchActions, which streams updates to the UI or CLI. If you are filtering for "failed" actions, the runStateManager uses the MatchingDescendantCount to ensure that parent actions remain visible in the tree if any of their children match your filter.

Lifecycle Operations

You interact with these entities primarily through the RunService (runs/service/run_service.go).

  1. CreateRun: Initializes the Run (the a0 action) and enqueues it for execution.
  2. RecordAction: Used by the executor to report the start or completion of child actions. It updates the database and triggers state transitions.
  3. WatchActions: Opens a stream that uses the runStateManager to provide a hierarchical view of the execution's progress, allowing you to monitor the status of every action from the root a0 down to the leaf tasks.