diff --git a/cmd/plan/list.go b/cmd/plan/list.go
index 6e47e7826..d4c07c32e 100644
--- a/cmd/plan/list.go
+++ b/cmd/plan/list.go
@@ -18,7 +18,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"opendev.org/airship/airshipctl/pkg/config"
-	"opendev.org/airship/airshipctl/pkg/errors"
+	"opendev.org/airship/airshipctl/pkg/phase"
 )
 
 const (
@@ -29,12 +29,15 @@ List life-cycle plans which were defined in document model.
 
 // NewListCommand creates a command which prints available phase plans
 func NewListCommand(cfgFactory config.Factory) *cobra.Command {
+	planCmd := &phase.PlanListCommand{Factory: cfgFactory}
+
 	listCmd := &cobra.Command{
 		Use:   "list",
 		Short: "List plans",
 		Long:  listLong[1:],
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return errors.ErrNotImplemented{What: "airshipctl plan list"}
+			planCmd.Writer = cmd.OutOrStdout()
+			return planCmd.RunE()
 		},
 	}
 	return listCmd
diff --git a/manifests/phases/plan.yaml b/manifests/phases/plan.yaml
index 2c5848598..26c16b92f 100644
--- a/manifests/phases/plan.yaml
+++ b/manifests/phases/plan.yaml
@@ -2,6 +2,7 @@ apiVersion: airshipit.org/v1alpha1
 kind: PhasePlan
 metadata:
   name: phasePlan
+description: "Default phase plan"
 phaseGroups:
   - name: group1
     phases:
diff --git a/pkg/api/v1alpha1/phaseplan_types.go b/pkg/api/v1alpha1/phaseplan_types.go
index 864a13b14..5c7e2543e 100644
--- a/pkg/api/v1alpha1/phaseplan_types.go
+++ b/pkg/api/v1alpha1/phaseplan_types.go
@@ -24,6 +24,7 @@ import (
 type PhasePlan struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
+	Description       string       `json:"description,omitempty"`
 	PhaseGroups       []PhaseGroup `json:"phaseGroups,omitempty"`
 }
 
diff --git a/pkg/phase/command.go b/pkg/phase/command.go
index e7d652467..1df54f841 100644
--- a/pkg/phase/command.go
+++ b/pkg/phase/command.go
@@ -15,12 +15,17 @@
 package phase
 
 import (
+	"fmt"
 	"io"
 	"os"
 	"path/filepath"
 	"strings"
 	"time"
 
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/cli-utils/pkg/print/table"
+
+	"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
 	"opendev.org/airship/airshipctl/pkg/config"
 	"opendev.org/airship/airshipctl/pkg/document"
 	"opendev.org/airship/airshipctl/pkg/phase/ifc"
@@ -146,3 +151,54 @@ func (c *TreeCommand) RunE() error {
 	t.PrintTree("")
 	return nil
 }
+
+// PlanListCommand phase list command
+type PlanListCommand struct {
+	Factory config.Factory
+	Writer  io.Writer
+}
+
+// RunE runs a phase plan command
+func (c *PlanListCommand) RunE() error {
+	cfg, err := c.Factory()
+	if err != nil {
+		return err
+	}
+
+	helper, err := NewHelper(cfg)
+	if err != nil {
+		return err
+	}
+
+	phases, err := helper.ListPlans()
+	if err != nil {
+		return err
+	}
+
+	rt, err := util.NewResourceTable(phases, util.DefaultStatusFunction())
+	if err != nil {
+		return err
+	}
+
+	printer := util.DefaultTablePrinter(c.Writer, nil)
+	descriptionCol := table.ColumnDef{
+		ColumnName:   "description",
+		ColumnHeader: "DESCRIPTION",
+		ColumnWidth:  40,
+		PrintResourceFunc: func(w io.Writer, width int, r table.Resource) (int, error) {
+			rs := r.ResourceStatus()
+			if rs == nil {
+				return 0, nil
+			}
+			plan := &v1alpha1.PhasePlan{}
+			err := runtime.DefaultUnstructuredConverter.FromUnstructured(rs.Resource.Object, plan)
+			if err != nil {
+				return 0, err
+			}
+			return fmt.Fprint(w, plan.Description)
+		},
+	}
+	printer.Columns = append(printer.Columns, descriptionCol)
+	printer.PrintTable(rt, 0)
+	return nil
+}
diff --git a/pkg/phase/command_test.go b/pkg/phase/command_test.go
index afa7dd410..541a458c8 100644
--- a/pkg/phase/command_test.go
+++ b/pkg/phase/command_test.go
@@ -15,7 +15,9 @@
 package phase_test
 
 import (
+	"bytes"
 	"fmt"
+	"io/ioutil"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -101,7 +103,7 @@ func TestRunCommand(t *testing.T) {
 	}
 }
 
-func TestPlanCommand(t *testing.T) {
+func TestListCommand(t *testing.T) {
 	tests := []struct {
 		name        string
 		errContains string
@@ -236,3 +238,58 @@ func TestTreeCommand(t *testing.T) {
 		})
 	}
 }
+
+func TestPlanListCommand(t *testing.T) {
+	testErr := fmt.Errorf(testFactoryErr)
+	testCases := []struct {
+		name        string
+		factory     config.Factory
+		expectedOut [][]byte
+		expectedErr string
+	}{
+		{
+			name: "Error config factory",
+			factory: func() (*config.Config, error) {
+				return nil, testErr
+			},
+			expectedErr: testFactoryErr,
+			expectedOut: [][]byte{{}},
+		},
+		{
+			name: "List phases",
+			factory: func() (*config.Config, error) {
+				conf := config.NewConfig()
+				manifest := conf.Manifests[config.AirshipDefaultManifest]
+				manifest.TargetPath = "testdata"
+				manifest.MetadataPath = "metadata.yaml"
+				manifest.Repositories[config.DefaultTestPhaseRepo].URLString = ""
+				return conf, nil
+			},
+			expectedOut: [][]byte{
+				[]byte("NAMESPACE   RESOURCE                                  DESCRIPTION                             "),
+				[]byte("            PhasePlan/phasePlan                       Default phase plan                      "),
+				{},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		tt := tc
+		t.Run(tt.name, func(t *testing.T) {
+			buf := &bytes.Buffer{}
+			cmd := phase.PlanListCommand{
+				Factory: tt.factory,
+				Writer:  buf,
+			}
+			err := cmd.RunE()
+			if tt.expectedErr != "" {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tt.expectedErr)
+			} else {
+				assert.NoError(t, err)
+			}
+			out, err := ioutil.ReadAll(buf)
+			require.NoError(t, err)
+			assert.Equal(t, tt.expectedOut, bytes.Split(out, []byte("\n")))
+		})
+	}
+}
diff --git a/pkg/phase/helper.go b/pkg/phase/helper.go
index fd0bffedf..d6d79ff55 100644
--- a/pkg/phase/helper.go
+++ b/pkg/phase/helper.go
@@ -15,8 +15,6 @@
 package phase
 
 import (
-	"fmt"
-	"io"
 	"path/filepath"
 
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -144,6 +142,35 @@ func (helper *Helper) ListPhases() ([]*v1alpha1.Phase, error) {
 	return phases, nil
 }
 
+// ListPlans returns all phases associated with manifest
+func (helper *Helper) ListPlans() ([]*v1alpha1.PhasePlan, error) {
+	bundle, err := document.NewBundleByPath(helper.phaseBundleRoot)
+	if err != nil {
+		return nil, err
+	}
+
+	plan := &v1alpha1.PhasePlan{}
+	selector, err := document.NewSelector().ByObject(plan, v1alpha1.Scheme)
+	if err != nil {
+		return nil, err
+	}
+
+	docs, err := bundle.Select(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	plans := make([]*v1alpha1.PhasePlan, len(docs))
+	for i, doc := range docs {
+		p := &v1alpha1.PhasePlan{}
+		if err = doc.ToAPIObject(p, v1alpha1.Scheme); err != nil {
+			return nil, err
+		}
+		plans[i] = p
+	}
+	return plans, nil
+}
+
 // ClusterMapAPIobj associated with the the manifest
 func (helper *Helper) ClusterMapAPIobj() (*v1alpha1.ClusterMap, error) {
 	bundle, err := document.NewBundleByPath(helper.phaseBundleRoot)
@@ -236,27 +263,3 @@ func (helper *Helper) PhaseEntryPointBasePath() string {
 func (helper *Helper) WorkDir() (string, error) {
 	return filepath.Join(util.UserHomeDir(), config.AirshipConfigDir), nil
 }
-
-// PrintPlan prints plan
-// TODO make this more readable in the future, and move to client
-func PrintPlan(plan *v1alpha1.PhasePlan, w io.Writer) error {
-	result := make(map[string][]string)
-	for _, phaseGroup := range plan.PhaseGroups {
-		phases := make([]string, len(phaseGroup.Phases))
-		for i, phase := range phaseGroup.Phases {
-			phases[i] = phase.Name
-		}
-		result[phaseGroup.Name] = phases
-	}
-
-	tw := util.NewTabWriter(w)
-	defer tw.Flush()
-	fmt.Fprintf(tw, "GROUP\tPHASE\n")
-	for group, phaseList := range result {
-		fmt.Fprintf(tw, "%s\t\n", group)
-		for _, phase := range phaseList {
-			fmt.Fprintf(tw, "\t%s\n", phase)
-		}
-	}
-	return nil
-}
diff --git a/pkg/phase/helper_test.go b/pkg/phase/helper_test.go
index b5027f04a..c991213a0 100644
--- a/pkg/phase/helper_test.go
+++ b/pkg/phase/helper_test.go
@@ -15,7 +15,6 @@
 package phase_test
 
 import (
-	"bytes"
 	"path/filepath"
 	"testing"
 
@@ -236,6 +235,57 @@ func TestHelperListPhases(t *testing.T) {
 	}
 }
 
+func TestHelperListPlans(t *testing.T) {
+	testCases := []struct {
+		name        string
+		errContains string
+		expectedLen int
+		config      func(t *testing.T) *config.Config
+	}{
+		{
+			name:        "Success plan list",
+			expectedLen: 1,
+			config:      testConfig,
+		},
+		{
+			name: "Error bundle path doesn't exist",
+			config: func(t *testing.T) *config.Config {
+				conf := testConfig(t)
+				conf.Manifests["dummy_manifest"].MetadataPath = brokenMetaPath
+				return conf
+			},
+			errContains: "no such file or directory",
+		},
+		{
+			name: "Success 0 plans",
+			config: func(t *testing.T) *config.Config {
+				conf := testConfig(t)
+				conf.Manifests["dummy_manifest"].MetadataPath = noPlanMetaPath
+				return conf
+			},
+			expectedLen: 0,
+		},
+	}
+
+	for _, test := range testCases {
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			helper, err := phase.NewHelper(tt.config(t))
+			require.NoError(t, err)
+			require.NotNil(t, helper)
+
+			actualList, actualErr := helper.ListPlans()
+			if tt.errContains != "" {
+				require.Error(t, actualErr)
+				assert.Contains(t, actualErr.Error(), tt.errContains)
+			} else {
+				require.NoError(t, actualErr)
+				assert.Len(t, actualList, tt.expectedLen)
+			}
+		})
+	}
+}
+
 func TestHelperClusterMapAPI(t *testing.T) {
 	testCases := []struct {
 		name         string
@@ -401,24 +451,6 @@ func TestHelperExecutorDoc(t *testing.T) {
 	}
 }
 
-func TestHelperPrintPlan(t *testing.T) {
-	helper, err := phase.NewHelper(testConfig(t))
-	require.NoError(t, err)
-	require.NotNil(t, helper)
-	plan, err := helper.Plan()
-	require.NoError(t, err)
-	require.NotNil(t, plan)
-	buf := bytes.NewBuffer([]byte{})
-	err = phase.PrintPlan(plan, buf)
-	require.NoError(t, err)
-	// easy check to make sure printed plan contains all phases in plan
-	assert.Contains(t, buf.String(), "remotedirect")
-	assert.Contains(t, buf.String(), "isogen")
-	assert.Contains(t, buf.String(), "initinfra")
-	assert.Contains(t, buf.String(), "some_phase")
-	assert.Contains(t, buf.String(), "capi_init")
-}
-
 func TestHelperTargetPath(t *testing.T) {
 	helper, err := phase.NewHelper(testConfig(t))
 	require.NoError(t, err)
diff --git a/pkg/phase/ifc/helper.go b/pkg/phase/ifc/helper.go
index 8431048c6..f4409a99b 100644
--- a/pkg/phase/ifc/helper.go
+++ b/pkg/phase/ifc/helper.go
@@ -29,6 +29,7 @@ type Helper interface {
 	Phase(phaseID ID) (*v1alpha1.Phase, error)
 	Plan() (*v1alpha1.PhasePlan, error)
 	ListPhases() ([]*v1alpha1.Phase, error)
+	ListPlans() ([]*v1alpha1.PhasePlan, error)
 	ClusterMapAPIobj() (*v1alpha1.ClusterMap, error)
 	ClusterMap() (clustermap.ClusterMap, error)
 	ExecutorDoc(phaseID ID) (document.Document, error)
diff --git a/pkg/phase/testdata/phases/kustomization.yaml b/pkg/phase/testdata/phases/kustomization.yaml
index 7d5a69471..fa8e1c108 100755
--- a/pkg/phase/testdata/phases/kustomization.yaml
+++ b/pkg/phase/testdata/phases/kustomization.yaml
@@ -1,4 +1,5 @@
 resources:
   - phases.yaml
   - executors.yaml
-  - cluster-map.yaml
\ No newline at end of file
+  - cluster-map.yaml
+  - phaseplan.yaml
\ No newline at end of file
diff --git a/pkg/phase/testdata/phases/phaseplan.yaml b/pkg/phase/testdata/phases/phaseplan.yaml
new file mode 100644
index 000000000..414efb0d3
--- /dev/null
+++ b/pkg/phase/testdata/phases/phaseplan.yaml
@@ -0,0 +1,9 @@
+apiVersion: airshipit.org/v1alpha1
+kind: PhasePlan
+metadata:
+  name: phasePlan
+description: "Default phase plan"
+phaseGroups:
+  - name: group1
+    phases:
+      - name: phase
\ No newline at end of file