Skip to main content

Understanding Runs and Actions

In Flyte, the concepts of a "Run" and an "Action" are unified into a single hierarchical model. While a Run represents the top-level execution of a workflow or task, it is technically implemented as a root Action. This design allows Flyte to treat every step of an execution—from the initial trigger to nested sub-tasks—using a consistent set of logic and database schemas.

The Unified Action Model

The core entity in this system is the Action class, defined in runs/repository/models/action.go. A Run is simply a type alias for an Action where the ParentActionName is null.

// From runs/repository/models/action.go

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

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"`

// ... other fields
}

When you initiate a workflow, Flyte creates a root action to represent the entire execution. By convention, this root action is always assigned the name a0. This constant is defined in runs/service/run_service.go:

const RootActionName = "a0"

Creating a Run

When a user or a schedule triggers an execution, RunService.CreateRun initializes the Run model. Because a Run is the top-level entity, its Name is set to RootActionName ("a0"), and it has no parent.

Internally, RunService populates the model with initial metadata and a QUEUED phase:

// Example of Run initialization in runs/service/run_service.go
runModel := &models.Run{
Project: runId.GetProject(),
Domain: runId.GetDomain(),
RunName: runId.GetName(),
Name: RootActionName, // "a0"
Phase: int32(common.ActionPhase_ACTION_PHASE_QUEUED),
ActionType: int32(workflow.ActionType_ACTION_TYPE_TASK),
ActionSpec: actionSpecBytes,
ActionDetails: []byte("{}"),
Attempts: 1,
RunSource: source.String(),
}

Hierarchical Execution

Actions can spawn child actions, creating a tree structure. This is common in complex workflows where a single task might trigger multiple sub-steps or retries. The relationship is maintained via the ParentActionName field.

The system uses this hierarchy to manage the lifecycle of resources. For example, in actions/k8s/client.go, Flyte uses the parent-child relationship to set Kubernetes OwnerReferences. This ensures that if a parent action is deleted, all its child actions (represented as TaskAction CRDs in Kubernetes) are also cleaned up by the Kubernetes garbage collector:

// From actions/k8s/client.go
isRoot := action.ParentActionName == nil || *action.ParentActionName == ""

if !isRoot {
parentID := &common.ActionIdentifier{
Run: actionID.Run,
Name: *action.ParentActionName,
}
// Logic to set OwnerReference to parent for K8s cascading deletion
}

State and Metadata Persistence

The Action model acts as a container for both the intended configuration and the actual runtime results. These are stored as serialized protobuf messages in the database:

  • ActionSpec: Contains the full specification of what the action is supposed to do (e.g., the task definition, resource requirements).
  • ActionDetails: Stores the runtime state, including the current phase, timestamps, error messages, and an array of ActionAttempts.
  • RunSpec: Specifically for root actions, this stores execution-wide settings like labels, annotations, and environment variables.

As an action progresses from QUEUED to RUNNING and finally to a terminal state like SUCCEEDED or FAILED, RunService updates these serialized blobs to reflect the current truth of the execution.

Summary of Identifiers

To uniquely identify any action in Flyte, the system uses a combination of four fields:

  1. Project: The organizational project.
  2. Domain: The environment (e.g., development, production).
  3. RunName: The unique ID for the entire execution tree.
  4. Name: The specific identifier for the action within that tree (e.g., a0 for the root, or generated IDs for children).