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:
- Project: The organizational project.
- Domain: The environment (e.g., development, production).
- RunName: The unique ID for the entire execution tree.
- Name: The specific identifier for the action within that tree (e.g.,
a0for the root, or generated IDs for children).