diff --git a/cmd/damon.go b/cmd/daemon.go
similarity index 69%
rename from cmd/damon.go
rename to cmd/daemon.go
index 9971373..8c0fbfd 100644
--- a/cmd/damon.go
+++ b/cmd/daemon.go
@@ -15,11 +15,11 @@ import (
 	"golang.org/x/sync/errgroup"
 )
 
-func runDaemon(ctx context.Context, input *Input) func(cmd *cobra.Command, args []string) error {
+func runDaemon(ctx context.Context, task *runtime.Task) func(cmd *cobra.Command, args []string) error {
 	return func(cmd *cobra.Command, args []string) error {
 		log.Infoln("Starting runner daemon")
 
-		_ = godotenv.Load(input.envFile)
+		_ = godotenv.Load(task.Input.EnvFile)
 		cfg, err := fromEnviron()
 		if err != nil {
 			log.WithError(err).
@@ -28,36 +28,10 @@ func runDaemon(ctx context.Context, input *Input) func(cmd *cobra.Command, args
 
 		initLogging(cfg)
 
-		engine, err := engine.New()
-		if err != nil {
-			log.WithError(err).
-				Fatalln("cannot load the docker engine")
-		}
-
-		count := 0
-		for {
-			err := engine.Ping(ctx)
-			if err == context.Canceled {
-				break
-			}
-			select {
-			case <-ctx.Done():
-				return ctx.Err()
-			default:
-			}
-			if err != nil {
-				log.WithError(err).
-					Errorln("cannot ping the docker daemon")
-				count++
-				if count == 5 {
-					log.WithError(err).
-						Fatalf("retry count reached: %d", count)
-				}
-				time.Sleep(time.Second)
-			} else {
-				log.Infoln("successfully pinged the docker daemon")
-				break
-			}
+		// try to connect to docker daemon
+		// if failed, exit with error
+		if err := engine.Start(ctx); err != nil {
+			log.WithError(err).Fatalln("failed to connect docker daemon engine")
 		}
 
 		cli := client.New(
@@ -81,6 +55,7 @@ func runDaemon(ctx context.Context, input *Input) func(cmd *cobra.Command, args
 			if err != nil {
 				log.WithError(err).
 					Errorln("cannot ping the remote server")
+				// TODO: if ping failed, retry or exit
 				time.Sleep(time.Second)
 			} else {
 				log.Infoln("successfully pinged the remote server")
diff --git a/cmd/root.go b/cmd/root.go
index 0ba2198..186b1bb 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -2,66 +2,17 @@ package cmd
 
 import (
 	"context"
-	"fmt"
 	"os"
-	"path/filepath"
 
+	"gitea.com/gitea/act_runner/engine"
+	"gitea.com/gitea/act_runner/runtime"
 	"github.com/mattn/go-isatty"
-	"github.com/nektos/act/pkg/artifacts"
-	"github.com/nektos/act/pkg/common"
-	"github.com/nektos/act/pkg/model"
-	"github.com/nektos/act/pkg/runner"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 )
 
 const version = "0.1"
 
-type Input struct {
-	envFile string
-	actor   string
-	// workdir string
-	// workflowsPath         string
-	// autodetectEvent       bool
-	// eventPath       string
-	reuseContainers bool
-	bindWorkdir     bool
-	// secrets         []string
-	// envs []string
-	// platforms       []string
-	// dryrun       bool
-	forcePull    bool
-	forceRebuild bool
-	// noOutput     bool
-	// envfile         string
-	// secretfile            string
-	insecureSecrets bool
-	// defaultBranch         string
-	privileged            bool
-	usernsMode            string
-	containerArchitecture string
-	containerDaemonSocket string
-	// noWorkflowRecurse     bool
-	useGitIgnore       bool
-	forgeInstance      string
-	containerCapAdd    []string
-	containerCapDrop   []string
-	autoRemove         bool
-	artifactServerPath string
-	artifactServerPort string
-	jsonLogger         bool
-	noSkipCheckout     bool
-	// remoteName            string
-}
-
-func (i *Input) newPlatforms() map[string]string {
-	return map[string]string{
-		"ubuntu-latest": "node:16-buster-slim",
-		"ubuntu-20.04":  "node:16-buster-slim",
-		"ubuntu-18.04":  "node:16-buster-slim",
-	}
-}
-
 // initLogging setup the global logrus logger.
 func initLogging(cfg Config) {
 	isTerm := isatty.IsTerminal(os.Stdout.Fd())
@@ -79,161 +30,52 @@ func initLogging(cfg Config) {
 }
 
 func Execute(ctx context.Context) {
-	input := Input{
-		reuseContainers: true,
-		forgeInstance:   "gitea.com",
-	}
 
+	task := runtime.NewTask()
+
+	// ./act_runner
 	rootCmd := &cobra.Command{
 		Use:          "act [event name to run]\nIf no event name passed, will default to \"on: push\"",
 		Short:        "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
 		Args:         cobra.MaximumNArgs(1),
-		RunE:         runCommand(ctx, &input),
+		RunE:         runRoot(ctx, task),
 		Version:      version,
 		SilenceUsage: true,
 	}
-	rootCmd.AddCommand(&cobra.Command{
+	rootCmd.Flags().BoolP("run", "r", false, "run workflows")
+	rootCmd.Flags().StringP("job", "j", "", "run job")
+	rootCmd.PersistentFlags().StringVarP(&task.Input.ForgeInstance, "forge-instance", "", "github.com", "Forge instance to use.")
+	rootCmd.PersistentFlags().StringVarP(&task.Input.EnvFile, "env-file", "", ".env", "Read in a file of environment variables.")
+
+	// ./act_runner daemon
+	daemonCmd := &cobra.Command{
 		Aliases: []string{"daemon"},
 		Use:     "daemon [event name to run]\nIf no event name passed, will default to \"on: push\"",
 		Short:   "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.",
 		Args:    cobra.MaximumNArgs(1),
-		RunE:    runDaemon(ctx, &input),
-	})
-	rootCmd.Flags().BoolP("run", "r", false, "run workflows")
-	rootCmd.Flags().StringP("job", "j", "", "run job")
-	rootCmd.PersistentFlags().StringVarP(&input.forgeInstance, "forge-instance", "", "github.com", "Forge instance to use.")
-	rootCmd.PersistentFlags().StringVarP(&input.envFile, "env-file", "", ".env", "Read in a file of environment variables.")
+		RunE:    runDaemon(ctx, task),
+	}
+	rootCmd.AddCommand(daemonCmd)
 
 	if err := rootCmd.Execute(); err != nil {
 		os.Exit(1)
 	}
 }
 
-// getWorkflowsPath return the workflows directory, it will try .gitea first and then fallback to .github
-func getWorkflowsPath() (string, error) {
-	dir, err := os.Getwd()
-	if err != nil {
-		return "", err
-	}
-	p := filepath.Join(dir, ".gitea/workflows")
-	_, err = os.Stat(p)
-	if err != nil {
-		if !os.IsNotExist(err) {
-			return "", err
-		}
-		return filepath.Join(dir, ".github/workflows"), nil
-	}
-	return p, nil
-}
-
-func runTask(ctx context.Context, input *Input, jobID string) error {
-	workflowsPath, err := getWorkflowsPath()
-	if err != nil {
-		return err
-	}
-	planner, err := model.NewWorkflowPlanner(workflowsPath, false)
-	if err != nil {
-		return err
-	}
-
-	var eventName string
-	events := planner.GetEvents()
-	if len(events) > 0 {
-		// set default event type to first event
-		// this way user dont have to specify the event.
-		log.Debugf("Using detected workflow event: %s", events[0])
-		eventName = events[0]
-	} else {
-		if plan := planner.PlanEvent("push"); plan != nil {
-			eventName = "push"
-		}
-	}
-
-	// build the plan for this run
-	var plan *model.Plan
-	if jobID != "" {
-		log.Debugf("Planning job: %s", jobID)
-		plan = planner.PlanJob(jobID)
-	} else {
-		log.Debugf("Planning event: %s", eventName)
-		plan = planner.PlanEvent(eventName)
-	}
-
-	curDir, err := os.Getwd()
-	if err != nil {
-		return err
-	}
-
-	// run the plan
-	config := &runner.Config{
-		Actor:           input.actor,
-		EventName:       eventName,
-		EventPath:       "",
-		DefaultBranch:   "",
-		ForcePull:       input.forcePull,
-		ForceRebuild:    input.forceRebuild,
-		ReuseContainers: input.reuseContainers,
-		Workdir:         curDir,
-		BindWorkdir:     input.bindWorkdir,
-		LogOutput:       true,
-		JSONLogger:      input.jsonLogger,
-		// Env:                   envs,
-		// Secrets:               secrets,
-		InsecureSecrets:       input.insecureSecrets,
-		Platforms:             input.newPlatforms(),
-		Privileged:            input.privileged,
-		UsernsMode:            input.usernsMode,
-		ContainerArchitecture: input.containerArchitecture,
-		ContainerDaemonSocket: input.containerDaemonSocket,
-		UseGitIgnore:          input.useGitIgnore,
-		GitHubInstance:        input.forgeInstance,
-		ContainerCapAdd:       input.containerCapAdd,
-		ContainerCapDrop:      input.containerCapDrop,
-		AutoRemove:            input.autoRemove,
-		ArtifactServerPath:    input.artifactServerPath,
-		ArtifactServerPort:    input.artifactServerPort,
-		NoSkipCheckout:        input.noSkipCheckout,
-		// RemoteName:            input.remoteName,
-	}
-	r, err := runner.New(config)
-	if err != nil {
-		return fmt.Errorf("New config failed: %v", err)
-	}
-
-	cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerPort)
-
-	executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
-		cancel()
-		return nil
-	})
-
-	outputHook := new(taskLogHook)
-	ctx = common.WithLoggerHook(ctx, outputHook)
-	return executor(ctx)
-}
-
-type taskLogHook struct{}
-
-func (h *taskLogHook) Levels() []log.Level {
-	return log.AllLevels
-}
-
-func (h *taskLogHook) Fire(entry *log.Entry) error {
-	if flag, ok := entry.Data["raw_output"]; ok {
-		if flagVal, ok := flag.(bool); flagVal && ok {
-			log.Infof("task log: %s", entry.Message)
-		}
-	}
-	return nil
-}
-
-func runCommand(ctx context.Context, input *Input) func(cmd *cobra.Command, args []string) error {
+func runRoot(ctx context.Context, task *runtime.Task) func(cmd *cobra.Command, args []string) error {
 	return func(cmd *cobra.Command, args []string) error {
 		jobID, err := cmd.Flags().GetString("job")
 		if err != nil {
 			return err
 		}
 
-		return runTask(ctx, input, jobID)
+		// try to connect to docker daemon
+		// if failed, exit with error
+		if err := engine.Start(ctx); err != nil {
+			log.WithError(err).Fatalln("failed to connect docker daemon engine")
+		}
+
+		task.JobID = jobID
+		return task.Run(ctx)
 	}
 }
diff --git a/engine/engine.go b/engine/engine.go
new file mode 100644
index 0000000..6a37b4b
--- /dev/null
+++ b/engine/engine.go
@@ -0,0 +1,43 @@
+package engine
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+)
+
+// Start start docker engine api loop
+func Start(ctx context.Context) error {
+	engine, err := New()
+	if err != nil {
+		return err
+	}
+
+	count := 0
+	for {
+		err := engine.Ping(ctx)
+		if err == context.Canceled {
+			break
+		}
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+		}
+		if err != nil {
+			log.WithError(err).
+				Errorln("cannot ping the docker daemon")
+			count++
+			if count == 5 {
+				return fmt.Errorf("retry connect to docker daemon failed: %d times", count)
+			}
+			time.Sleep(time.Second)
+		} else {
+			log.Infoln("successfully ping the docker daemon")
+			break
+		}
+	}
+	return nil
+}
diff --git a/runtime/task.go b/runtime/task.go
new file mode 100644
index 0000000..1f632f1
--- /dev/null
+++ b/runtime/task.go
@@ -0,0 +1,202 @@
+package runtime
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/nektos/act/pkg/artifacts"
+	"github.com/nektos/act/pkg/common"
+	"github.com/nektos/act/pkg/model"
+	"github.com/nektos/act/pkg/runner"
+	log "github.com/sirupsen/logrus"
+)
+
+type TaskInput struct {
+	repoDirectory string
+	actor         string
+	// workdir string
+	// workflowsPath         string
+	// autodetectEvent       bool
+	// eventPath       string
+	reuseContainers bool
+	bindWorkdir     bool
+	// secrets         []string
+	// envs []string
+	// platforms       []string
+	// dryrun       bool
+	forcePull    bool
+	forceRebuild bool
+	// noOutput     bool
+	// envfile         string
+	// secretfile            string
+	insecureSecrets bool
+	// defaultBranch         string
+	privileged            bool
+	usernsMode            string
+	containerArchitecture string
+	containerDaemonSocket string
+	// noWorkflowRecurse     bool
+	useGitIgnore       bool
+	containerCapAdd    []string
+	containerCapDrop   []string
+	autoRemove         bool
+	artifactServerPath string
+	artifactServerPort string
+	jsonLogger         bool
+	noSkipCheckout     bool
+	// remoteName            string
+
+	ForgeInstance string
+	EnvFile       string
+}
+
+type taskLogHook struct {
+	entries []*log.Entry
+}
+
+func (h *taskLogHook) Levels() []log.Level {
+	return log.AllLevels
+}
+
+func (h *taskLogHook) Fire(entry *log.Entry) error {
+	if flag, ok := entry.Data["raw_output"]; ok {
+		if flagVal, ok := flag.(bool); flagVal && ok {
+			log.Infof("task log: %s", entry.Message)
+			h.entries = append(h.entries, entry)
+		}
+	}
+	return nil
+}
+
+type Task struct {
+	JobID   string
+	Input   *TaskInput
+	LogHook *taskLogHook
+}
+
+func NewTask() *Task {
+	task := &Task{
+		Input: &TaskInput{
+			reuseContainers: true,
+			ForgeInstance:   "gitea",
+		},
+		LogHook: &taskLogHook{},
+	}
+	task.Input.repoDirectory, _ = os.Getwd()
+	return task
+}
+
+// getWorkflowsPath return the workflows directory, it will try .gitea first and then fallback to .github
+func getWorkflowsPath(dir string) (string, error) {
+	p := filepath.Join(dir, ".gitea/workflows")
+	_, err := os.Stat(p)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			return "", err
+		}
+		return filepath.Join(dir, ".github/workflows"), nil
+	}
+	return p, nil
+}
+
+func demoPlatforms() map[string]string {
+	return map[string]string{
+		"ubuntu-latest": "node:16-buster-slim",
+		"ubuntu-20.04":  "node:16-buster-slim",
+		"ubuntu-18.04":  "node:16-buster-slim",
+	}
+}
+
+func (t *Task) Run(ctx context.Context) error {
+	workflowsPath, err := getWorkflowsPath(t.Input.repoDirectory)
+	if err != nil {
+		return err
+	}
+	planner, err := model.NewWorkflowPlanner(workflowsPath, false)
+	if err != nil {
+		return err
+	}
+
+	var eventName string
+	events := planner.GetEvents()
+	if len(events) > 0 {
+		// set default event type to first event
+		// this way user dont have to specify the event.
+		log.Debugf("Using detected workflow event: %s", events[0])
+		eventName = events[0]
+	} else {
+		if plan := planner.PlanEvent("push"); plan != nil {
+			eventName = "push"
+		}
+	}
+
+	// build the plan for this run
+	var plan *model.Plan
+	var jobID = t.JobID
+	if jobID != "" {
+		log.Debugf("Planning job: %s", jobID)
+		plan = planner.PlanJob(jobID)
+	} else {
+		log.Debugf("Planning event: %s", eventName)
+		plan = planner.PlanEvent(eventName)
+	}
+
+	curDir, err := os.Getwd()
+	if err != nil {
+		return err
+	}
+
+	// run the plan
+	input := t.Input
+	config := &runner.Config{
+		Actor:           input.actor,
+		EventName:       eventName,
+		EventPath:       "",
+		DefaultBranch:   "",
+		ForcePull:       input.forcePull,
+		ForceRebuild:    input.forceRebuild,
+		ReuseContainers: input.reuseContainers,
+		Workdir:         curDir,
+		BindWorkdir:     input.bindWorkdir,
+		LogOutput:       true,
+		JSONLogger:      input.jsonLogger,
+		// Env:                   envs,
+		// Secrets:               secrets,
+		InsecureSecrets:       input.insecureSecrets,
+		Platforms:             demoPlatforms(),
+		Privileged:            input.privileged,
+		UsernsMode:            input.usernsMode,
+		ContainerArchitecture: input.containerArchitecture,
+		ContainerDaemonSocket: input.containerDaemonSocket,
+		UseGitIgnore:          input.useGitIgnore,
+		GitHubInstance:        input.ForgeInstance,
+		ContainerCapAdd:       input.containerCapAdd,
+		ContainerCapDrop:      input.containerCapDrop,
+		AutoRemove:            input.autoRemove,
+		ArtifactServerPath:    input.artifactServerPath,
+		ArtifactServerPort:    input.artifactServerPort,
+		NoSkipCheckout:        input.noSkipCheckout,
+		// RemoteName:            input.remoteName,
+	}
+	r, err := runner.New(config)
+	if err != nil {
+		return fmt.Errorf("new config failed: %v", err)
+	}
+
+	cancel := artifacts.Serve(ctx, input.artifactServerPath, input.artifactServerPort)
+
+	executor := r.NewPlanExecutor(plan).Finally(func(ctx context.Context) error {
+		cancel()
+		return nil
+	})
+
+	ctx = common.WithLoggerHook(ctx, t.LogHook)
+	if err := executor(ctx); err != nil {
+		log.Warnf("workflow execution failed:%v, logs: %d", err, len(t.LogHook.entries))
+		return err
+	}
+	log.Infof("workflow completed, logs: %d", len(t.LogHook.entries))
+	return nil
+}