diff --git a/pkg/phase/errors.go b/pkg/phase/errors.go
new file mode 100644
index 000000000..8739f66cc
--- /dev/null
+++ b/pkg/phase/errors.go
@@ -0,0 +1,31 @@
+/*
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package phase
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// ErrExecutorNotFound is returned if phase executor was not found in executor
+// registry map
+type ErrExecutorNotFound struct {
+	GVK schema.GroupVersionKind
+}
+
+func (e ErrExecutorNotFound) Error() string {
+	return fmt.Sprintf("executor identified by '%s' is not found", e.GVK)
+}
diff --git a/pkg/phase/ifc/executor.go b/pkg/phase/ifc/executor.go
new file mode 100644
index 000000000..f7af81f8e
--- /dev/null
+++ b/pkg/phase/ifc/executor.go
@@ -0,0 +1,42 @@
+/*
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ifc
+
+import (
+	"io"
+
+	"opendev.org/airship/airshipctl/pkg/document"
+	"opendev.org/airship/airshipctl/pkg/environment"
+)
+
+// Executor interface should be implemented by each runner
+type Executor interface {
+	Run(dryrun, debug bool) error
+	Render(io.Writer) error
+	Validate() error
+	Wait() error
+}
+
+// ExecutorFactory for executor instantiation
+// First argument is document object which represents executor
+// configuration.
+// Second argument is document bundle used by executor.
+// Third argument airship configuration settings since each phase
+// has to be aware of execution context and global settings
+type ExecutorFactory func(
+	document.Document,
+	document.Bundle,
+	*environment.AirshipCTLSettings,
+) (Executor, error)
diff --git a/pkg/phase/phase.go b/pkg/phase/phase.go
index 70cf35e5e..634642fa1 100644
--- a/pkg/phase/phase.go
+++ b/pkg/phase/phase.go
@@ -17,9 +17,13 @@ package phase
 import (
 	"path/filepath"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
 	airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
 	"opendev.org/airship/airshipctl/pkg/document"
 	"opendev.org/airship/airshipctl/pkg/environment"
+	"opendev.org/airship/airshipctl/pkg/phase/ifc"
 )
 
 const (
@@ -28,6 +32,11 @@ const (
 	PhaseDirName = "phases"
 )
 
+var (
+	// ExecutorRegistry contins registered runner factories
+	ExecutorRegistry = make(map[schema.GroupVersionKind]ifc.ExecutorFactory)
+)
+
 // Cmd object to work with phase api
 type Cmd struct {
 	*environment.AirshipCTLSettings
@@ -42,6 +51,84 @@ func (p *Cmd) getBundle() (document.Bundle, error) {
 	return document.NewBundleByPath(filepath.Join(ccm.TargetPath, ccm.SubPath, PhaseDirName))
 }
 
+// GetPhase returns particular phase object identified by name
+func (p *Cmd) GetPhase(name string) (*airshipv1.Phase, error) {
+	bundle, err := p.getBundle()
+	if err != nil {
+		return nil, err
+	}
+	phaseConfig := &airshipv1.Phase{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+	}
+	selector, err := document.NewSelector().ByObject(phaseConfig, airshipv1.Scheme)
+	if err != nil {
+		return nil, err
+	}
+	doc, err := bundle.SelectOne(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	if err = doc.ToAPIObject(phaseConfig, airshipv1.Scheme); err != nil {
+		return nil, err
+	}
+	return phaseConfig, nil
+}
+
+// GetExecutor referenced in a phase configuration
+func (p *Cmd) GetExecutor(phase *airshipv1.Phase) (ifc.Executor, error) {
+	bundle, err := p.getBundle()
+	if err != nil {
+		return nil, err
+	}
+	phaseConfig := phase.Config
+	// Searching executor configuration document referenced in
+	// phase configuration
+	refGVK := phaseConfig.ExecutorRef.GroupVersionKind()
+	selector := document.NewSelector().
+		ByGvk(refGVK.Group, refGVK.Version, refGVK.Kind).
+		ByName(phaseConfig.ExecutorRef.Name).
+		ByNamespace(phaseConfig.ExecutorRef.Namespace)
+	doc, err := bundle.SelectOne(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// Define executor configuration options
+	targetPath, err := p.Config.CurrentContextTargetPath()
+	if err != nil {
+		return nil, err
+	}
+	executorDocBundle, err := document.NewBundleByPath(filepath.Join(targetPath, phaseConfig.DocumentEntryPoint))
+	if err != nil {
+		return nil, err
+	}
+
+	// Look for executor factory defined in registry
+	executorFactory, found := ExecutorRegistry[refGVK]
+	if !found {
+		return nil, ErrExecutorNotFound{GVK: refGVK}
+	}
+	return executorFactory(doc, executorDocBundle, p.AirshipCTLSettings)
+}
+
+// Exec particular phase
+func (p *Cmd) Exec(name string) error {
+	phaseConfig, err := p.GetPhase(name)
+	if err != nil {
+		return err
+	}
+
+	executor, err := p.GetExecutor(phaseConfig)
+	if err != nil {
+		return err
+	}
+
+	return executor.Run(p.DryRun, p.Debug)
+}
+
 // Plan shows available phase names
 func (p *Cmd) Plan() (map[string][]string, error) {
 	bundle, err := p.getBundle()
diff --git a/pkg/phase/phase_test.go b/pkg/phase/phase_test.go
index a59ea9dc8..015247d98 100644
--- a/pkg/phase/phase_test.go
+++ b/pkg/phase/phase_test.go
@@ -19,13 +19,20 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
 	"sigs.k8s.io/kustomize/api/resid"
 	"sigs.k8s.io/kustomize/api/types"
 
+	airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
 	"opendev.org/airship/airshipctl/pkg/config"
 	"opendev.org/airship/airshipctl/pkg/document"
 	"opendev.org/airship/airshipctl/pkg/environment"
 	"opendev.org/airship/airshipctl/pkg/phase"
+	"opendev.org/airship/airshipctl/pkg/phase/ifc"
 )
 
 func TestPhasePlan(t *testing.T) {
@@ -52,6 +59,8 @@ func TestPhasePlan(t *testing.T) {
 					"isogen",
 					"remotedirect",
 					"initinfra",
+					"some_phase",
+					"capi_init",
 				},
 			},
 		},
@@ -89,6 +98,149 @@ func TestPhasePlan(t *testing.T) {
 	}
 }
 
+func TestGetPhase(t *testing.T) {
+	testCases := []struct {
+		name          string
+		settings      func() *environment.AirshipCTLSettings
+		phaseName     string
+		expectedPhase *airshipv1.Phase
+		expectedErr   error
+	}{
+		{
+			name: "No context",
+			settings: func() *environment.AirshipCTLSettings {
+				s := makeDefaultSettings()
+				s.Config.CurrentContext = "badCtx"
+				return s
+			},
+			expectedErr: config.ErrMissingConfig{What: "Context with name 'badCtx'"},
+		},
+		{
+			name:      "Get existing phase",
+			settings:  makeDefaultSettings,
+			phaseName: "capi_init",
+			expectedPhase: &airshipv1.Phase{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "airshipit.org/v1alpha1",
+					Kind:       "Phase",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "capi_init",
+				},
+				Config: airshipv1.PhaseConfig{
+					ExecutorRef: &corev1.ObjectReference{
+						Kind:       "Clusterctl",
+						APIVersion: "airshipit.org/v1alpha1",
+						Name:       "clusterctl-v1",
+					},
+					DocumentEntryPoint: "manifests/site/test-site/auth",
+				},
+			},
+		},
+		{
+			name:      "Get non-existing phase",
+			settings:  makeDefaultSettings,
+			phaseName: "some_name",
+			expectedErr: document.ErrDocNotFound{
+				Selector: document.Selector{
+					Selector: types.Selector{
+						Gvk: resid.Gvk{
+							Group:   "airshipit.org",
+							Version: "v1alpha1",
+							Kind:    "Phase",
+						},
+						Name: "some_name",
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			cmd := phase.Cmd{AirshipCTLSettings: tt.settings()}
+			actualPhase, actualErr := cmd.GetPhase(tt.phaseName)
+			assert.Equal(t, tt.expectedErr, actualErr)
+			assert.Equal(t, tt.expectedPhase, actualPhase)
+		})
+	}
+}
+
+func TestGetExecutor(t *testing.T) {
+	testCases := []struct {
+		name        string
+		settings    func() *environment.AirshipCTLSettings
+		phase       *airshipv1.Phase
+		expectedExc ifc.Executor
+		expectedErr error
+	}{
+		{
+			name: "No context",
+			settings: func() *environment.AirshipCTLSettings {
+				s := makeDefaultSettings()
+				s.Config.CurrentContext = "badCtx"
+				return s
+			},
+			expectedErr: config.ErrMissingConfig{What: "Context with name 'badCtx'"},
+		},
+		{
+			name:     "Get non-existing executor",
+			settings: makeDefaultSettings,
+			phase: &airshipv1.Phase{
+				Config: airshipv1.PhaseConfig{
+					ExecutorRef: &corev1.ObjectReference{
+						APIVersion: "example.com/v1",
+						Kind:       "SomeKind",
+					},
+				},
+			},
+			expectedErr: document.ErrDocNotFound{
+				Selector: document.Selector{
+					Selector: types.Selector{
+						Gvk: resid.Gvk{
+							Group:   "example.com",
+							Version: "v1",
+							Kind:    "SomeKind",
+						},
+					},
+				},
+			},
+		},
+		{
+			name:     "Get unregistered executor",
+			settings: makeDefaultSettings,
+			phase: &airshipv1.Phase{
+				Config: airshipv1.PhaseConfig{
+					ExecutorRef: &corev1.ObjectReference{
+						APIVersion: "airshipit.org/v1alpha1",
+						Kind:       "SomeExecutor",
+						Name:       "executor-name",
+					},
+					DocumentEntryPoint: "valid_site/phases",
+				},
+			},
+			expectedErr: phase.ErrExecutorNotFound{
+				GVK: schema.GroupVersionKind{
+					Group:   "airshipit.org",
+					Version: "v1alpha1",
+					Kind:    "SomeExecutor",
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			cmd := phase.Cmd{AirshipCTLSettings: tt.settings()}
+			actualExc, actualErr := cmd.GetExecutor(tt.phase)
+			assert.Equal(t, tt.expectedErr, actualErr)
+			assert.Equal(t, tt.expectedExc, actualExc)
+		})
+	}
+}
+
 func makeDefaultSettings() *environment.AirshipCTLSettings {
 	testSettings := &environment.AirshipCTLSettings{
 		AirshipConfigPath: "testdata/airshipconfig.yaml",
diff --git a/pkg/phase/testdata/valid_site/phases/capi_init.yaml b/pkg/phase/testdata/valid_site/phases/capi_init.yaml
new file mode 100644
index 000000000..68418ad0d
--- /dev/null
+++ b/pkg/phase/testdata/valid_site/phases/capi_init.yaml
@@ -0,0 +1,10 @@
+apiVersion: airshipit.org/v1alpha1
+kind: Phase
+metadata:
+  name: capi_init
+config:
+  executorRef:
+    apiVersion: airshipit.org/v1alpha1
+    kind: Clusterctl
+    name: clusterctl-v1
+  documentEntryPoint: manifests/site/test-site/auth
\ No newline at end of file
diff --git a/pkg/phase/testdata/valid_site/phases/clusterctl.yaml b/pkg/phase/testdata/valid_site/phases/clusterctl.yaml
new file mode 100644
index 000000000..3c939c15c
--- /dev/null
+++ b/pkg/phase/testdata/valid_site/phases/clusterctl.yaml
@@ -0,0 +1,12 @@
+apiVersion: airshipit.org/v1alpha1
+kind: Clusterctl
+metadata:
+  name: clusterctl-v1
+action: init
+init-options:
+  core-provider: "cluster-api:v0.3.3"
+providers:
+  - name: "cluster-api"
+    type: "CoreProvider"
+    versions:
+      v0.3.3: manifests/function/capi/v0.3.3
\ No newline at end of file
diff --git a/pkg/phase/testdata/valid_site/phases/kustomization.yaml b/pkg/phase/testdata/valid_site/phases/kustomization.yaml
index af85fe23b..5385fa3b4 100644
--- a/pkg/phase/testdata/valid_site/phases/kustomization.yaml
+++ b/pkg/phase/testdata/valid_site/phases/kustomization.yaml
@@ -1,2 +1,6 @@
 resources:
   - phaseplan.yaml
+  - some_phase.yaml
+  - some_exc.yaml
+  - capi_init.yaml
+  - clusterctl.yaml
diff --git a/pkg/phase/testdata/valid_site/phases/phaseplan.yaml b/pkg/phase/testdata/valid_site/phases/phaseplan.yaml
index 2848cb2ce..8e07734e9 100644
--- a/pkg/phase/testdata/valid_site/phases/phaseplan.yaml
+++ b/pkg/phase/testdata/valid_site/phases/phaseplan.yaml
@@ -8,3 +8,5 @@ phaseGroups:
       - name: isogen
       - name: remotedirect
       - name: initinfra
+      - name: some_phase
+      - name: capi_init
diff --git a/pkg/phase/testdata/valid_site/phases/some_exc.yaml b/pkg/phase/testdata/valid_site/phases/some_exc.yaml
new file mode 100644
index 000000000..162e72afa
--- /dev/null
+++ b/pkg/phase/testdata/valid_site/phases/some_exc.yaml
@@ -0,0 +1,13 @@
+apiVersion: airshipit.org/v1alpha1
+kind: SomeExecutor
+metadata:
+  labels:
+    airshipit.org/deploy-k8s: "false"
+  name: executor-name
+init-options:
+  core-provider: "cluster-api:v0.3.3"
+providers:
+  - name: "cluster-api"
+    type: "CoreProvider"
+    versions:
+      v0.3.3: manifests/function/capi/v0.3.3
\ No newline at end of file
diff --git a/pkg/phase/testdata/valid_site/phases/some_phase.yaml b/pkg/phase/testdata/valid_site/phases/some_phase.yaml
new file mode 100644
index 000000000..ee44a2f16
--- /dev/null
+++ b/pkg/phase/testdata/valid_site/phases/some_phase.yaml
@@ -0,0 +1,10 @@
+apiVersion: airshipit.org/v1alpha1
+kind: Phase
+metadata:
+  name: some_phase
+config:
+  executorRef:
+    apiVersion: airshipit.org/v1alpha1
+    kind: SomeExecutor
+    name: executor-name
+  documentEntryPoint: manifests/site/test-site/auth
\ No newline at end of file