diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index 0babe84410..c55e63db6b 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -494,3 +494,17 @@ type PackagePayload struct {
 func (p *PackagePayload) JSONPayload() ([]byte, error) {
 	return json.MarshalIndent(p, "", "  ")
 }
+
+// WorkflowDispatchPayload represents a workflow dispatch payload
+type WorkflowDispatchPayload struct {
+	Workflow   string         `json:"workflow"`
+	Ref        string         `json:"ref"`
+	Inputs     map[string]any `json:"inputs"`
+	Repository *Repository    `json:"repository"`
+	Sender     *User          `json:"sender"`
+}
+
+// JSONPayload implements Payload
+func (p *WorkflowDispatchPayload) JSONPayload() ([]byte, error) {
+	return json.MarshalIndent(p, "", "  ")
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index f3a7a38951..ef7628967c 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -628,6 +628,7 @@ org_still_own_repo = "This organization still owns one or more repositories, del
 org_still_own_packages = "This organization still owns one or more packages, delete them first."
 
 target_branch_not_exist = Target branch does not exist.
+target_ref_not_exist = Target ref does not exist %s
 
 admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first.
 
@@ -3701,6 +3702,11 @@ workflow.disable_success = Workflow '%s' disabled successfully.
 workflow.enable = Enable Workflow
 workflow.enable_success = Workflow '%s' enabled successfully.
 workflow.disabled = Workflow is disabled.
+workflow.run = Run Workflow
+workflow.not_found = Workflow '%s' not found.
+workflow.run_success = Workflow '%s' run successfully.
+workflow.from_ref = Use workflow from
+workflow.has_workflow_dispatch = This workflow has a workflow_dispatch event trigger.
 
 need_approval_desc = Need approval to run workflows for fork pull request.
 
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index a0f03ec7e9..63cf3e948a 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -7,22 +7,28 @@ import (
 	"bytes"
 	"fmt"
 	"net/http"
+	"slices"
 	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/web/repo"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
 
 	"github.com/nektos/act/pkg/model"
+	"gopkg.in/yaml.v3"
 )
 
 const (
@@ -58,8 +64,13 @@ func MustEnableActions(ctx *context.Context) {
 func List(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("actions.actions")
 	ctx.Data["PageIsActions"] = true
+	workflowID := ctx.FormString("workflow")
+	actorID := ctx.FormInt64("actor")
+	status := ctx.FormInt("status")
+	ctx.Data["CurWorkflow"] = workflowID
 
 	var workflows []Workflow
+	var curWorkflow *model.Workflow
 	if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
 		ctx.ServerError("IsEmpty", err)
 		return
@@ -140,6 +151,10 @@ func List(ctx *context.Context) {
 				workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
 			}
 			workflows = append(workflows, workflow)
+
+			if workflow.Entry.Name() == workflowID {
+				curWorkflow = wf
+			}
 		}
 	}
 	ctx.Data["workflows"] = workflows
@@ -150,17 +165,46 @@ func List(ctx *context.Context) {
 		page = 1
 	}
 
-	workflow := ctx.FormString("workflow")
-	actorID := ctx.FormInt64("actor")
-	status := ctx.FormInt("status")
-	ctx.Data["CurWorkflow"] = workflow
-
 	actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
 	ctx.Data["ActionsConfig"] = actionsConfig
 
-	if len(workflow) > 0 && ctx.Repo.IsAdmin() {
+	if len(workflowID) > 0 && ctx.Repo.IsAdmin() {
 		ctx.Data["AllowDisableOrEnableWorkflow"] = true
-		ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(workflow)
+		isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
+		ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
+
+		if !isWorkflowDisabled && curWorkflow != nil {
+			workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
+			if workflowDispatchConfig != nil {
+				ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
+
+				branchOpts := git_model.FindBranchOptions{
+					RepoID:          ctx.Repo.Repository.ID,
+					IsDeletedBranch: optional.Some(false),
+					ListOptions: db.ListOptions{
+						ListAll: true,
+					},
+				}
+				branches, err := git_model.FindBranchNames(ctx, branchOpts)
+				if err != nil {
+					ctx.ServerError("FindBranchNames", err)
+					return
+				}
+				// always put default branch on the top if it exists
+				if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
+					branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
+					branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
+				}
+				ctx.Data["Branches"] = branches
+
+				tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
+				if err != nil {
+					ctx.ServerError("GetTagNamesByRepoID", err)
+					return
+				}
+				ctx.Data["Tags"] = tags
+			}
+		}
 	}
 
 	// if status or actor query param is not given to frontend href, (href="/<repoLink>/actions")
@@ -177,7 +221,7 @@ func List(ctx *context.Context) {
 			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 		},
 		RepoID:        ctx.Repo.Repository.ID,
-		WorkflowID:    workflow,
+		WorkflowID:    workflowID,
 		TriggerUserID: actorID,
 	}
 
@@ -214,7 +258,7 @@ func List(ctx *context.Context) {
 
 	pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamString("workflow", workflow)
+	pager.AddParamString("workflow", workflowID)
 	pager.AddParamString("actor", fmt.Sprint(actorID))
 	pager.AddParamString("status", fmt.Sprint(status))
 	ctx.Data["Page"] = pager
@@ -222,3 +266,86 @@ func List(ctx *context.Context) {
 
 	ctx.HTML(http.StatusOK, tplListActions)
 }
+
+type WorkflowDispatchInput struct {
+	Name        string   `yaml:"name"`
+	Description string   `yaml:"description"`
+	Required    bool     `yaml:"required"`
+	Default     string   `yaml:"default"`
+	Type        string   `yaml:"type"`
+	Options     []string `yaml:"options"`
+}
+
+type WorkflowDispatch struct {
+	Inputs []WorkflowDispatchInput
+}
+
+func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
+	switch w.RawOn.Kind {
+	case yaml.ScalarNode:
+		var val string
+		if !decodeNode(w.RawOn, &val) {
+			return nil
+		}
+		if val == "workflow_dispatch" {
+			return &WorkflowDispatch{}
+		}
+	case yaml.SequenceNode:
+		var val []string
+		if !decodeNode(w.RawOn, &val) {
+			return nil
+		}
+		for _, v := range val {
+			if v == "workflow_dispatch" {
+				return &WorkflowDispatch{}
+			}
+		}
+	case yaml.MappingNode:
+		var val map[string]yaml.Node
+		if !decodeNode(w.RawOn, &val) {
+			return nil
+		}
+
+		workflowDispatchNode, found := val["workflow_dispatch"]
+		if !found {
+			return nil
+		}
+
+		var workflowDispatch WorkflowDispatch
+		var workflowDispatchVal map[string]yaml.Node
+		if !decodeNode(workflowDispatchNode, &workflowDispatchVal) {
+			return &workflowDispatch
+		}
+
+		inputsNode, found := workflowDispatchVal["inputs"]
+		if !found || inputsNode.Kind != yaml.MappingNode {
+			return &workflowDispatch
+		}
+
+		i := 0
+		for {
+			if i+1 >= len(inputsNode.Content) {
+				break
+			}
+			var input WorkflowDispatchInput
+			if decodeNode(*inputsNode.Content[i+1], &input) {
+				input.Name = inputsNode.Content[i].Value
+				workflowDispatch.Inputs = append(workflowDispatch.Inputs, input)
+			}
+			i += 2
+		}
+		return &workflowDispatch
+
+	default:
+		return nil
+	}
+	return nil
+}
+
+func decodeNode(node yaml.Node, out any) bool {
+	if err := node.Decode(out); err != nil {
+		log.Warn("Failed to decode node %v into %T: %v", node, out, err)
+		return false
+	}
+	return true
+}
diff --git a/routers/web/repo/actions/actions_test.go b/routers/web/repo/actions/actions_test.go
new file mode 100644
index 0000000000..194704d14e
--- /dev/null
+++ b/routers/web/repo/actions/actions_test.go
@@ -0,0 +1,156 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"strings"
+	"testing"
+
+	act_model "github.com/nektos/act/pkg/model"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
+	yaml := `
+    name: local-action-docker-url
+    `
+	workflow, err := act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch := workflowDispatchConfig(workflow)
+	assert.Nil(t, workflowDispatch)
+
+	yaml = `
+    name: local-action-docker-url
+    on: push
+    `
+	workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch = workflowDispatchConfig(workflow)
+	assert.Nil(t, workflowDispatch)
+
+	yaml = `
+    name: local-action-docker-url
+    on: workflow_dispatch
+    `
+	workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch = workflowDispatchConfig(workflow)
+	assert.NotNil(t, workflowDispatch)
+	assert.Nil(t, workflowDispatch.Inputs)
+
+	yaml = `
+    name: local-action-docker-url
+    on: [push, pull_request]
+    `
+	workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch = workflowDispatchConfig(workflow)
+	assert.Nil(t, workflowDispatch)
+
+	yaml = `
+    name: local-action-docker-url
+    on:
+        push:
+        pull_request:
+    `
+	workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch = workflowDispatchConfig(workflow)
+	assert.Nil(t, workflowDispatch)
+
+	yaml = `
+    name: local-action-docker-url
+    on: [push, workflow_dispatch]
+    `
+	workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch = workflowDispatchConfig(workflow)
+	assert.NotNil(t, workflowDispatch)
+	assert.Nil(t, workflowDispatch.Inputs)
+
+	yaml = `
+    name: local-action-docker-url
+    on:
+        - push
+        - workflow_dispatch
+    `
+	workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch = workflowDispatchConfig(workflow)
+	assert.NotNil(t, workflowDispatch)
+	assert.Nil(t, workflowDispatch.Inputs)
+
+	yaml = `
+    name: local-action-docker-url
+    on:
+        push:
+        pull_request:
+        workflow_dispatch:
+            inputs:
+    `
+	workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch = workflowDispatchConfig(workflow)
+	assert.NotNil(t, workflowDispatch)
+	assert.Nil(t, workflowDispatch.Inputs)
+
+	yaml = `
+    name: local-action-docker-url
+    on:
+        push:
+        pull_request:
+        workflow_dispatch:
+            inputs:
+                logLevel:
+                    description: 'Log level'
+                    required: true
+                    default: 'warning'
+                    type: choice
+                    options:
+                    - info
+                    - warning
+                    - debug
+                boolean_default_true:
+                    description: 'Test scenario tags'
+                    required: true
+                    type: boolean
+                    default: true
+                boolean_default_false:
+                    description: 'Test scenario tags'
+                    required: true
+                    type: boolean
+                    default: false
+    `
+
+	workflow, err = act_model.ReadWorkflow(strings.NewReader(yaml))
+	assert.NoError(t, err, "read workflow should succeed")
+	workflowDispatch = workflowDispatchConfig(workflow)
+	assert.NotNil(t, workflowDispatch)
+	assert.Equal(t, WorkflowDispatchInput{
+		Name:        "logLevel",
+		Default:     "warning",
+		Description: "Log level",
+		Options: []string{
+			"info",
+			"warning",
+			"debug",
+		},
+		Required: true,
+		Type:     "choice",
+	}, workflowDispatch.Inputs[0])
+	assert.Equal(t, WorkflowDispatchInput{
+		Name:        "boolean_default_true",
+		Default:     "true",
+		Description: "Test scenario tags",
+		Required:    true,
+		Type:        "boolean",
+	}, workflowDispatch.Inputs[1])
+	assert.Equal(t, WorkflowDispatchInput{
+		Name:        "boolean_default_false",
+		Default:     "false",
+		Description: "Test scenario tags",
+		Required:    true,
+		Type:        "boolean",
+	}, workflowDispatch.Inputs[2])
+}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 6b42289164..11199d69eb 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -18,18 +18,26 @@ import (
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/perm"
+	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
+	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	actions_service "code.gitea.io/gitea/services/actions"
 	context_module "code.gitea.io/gitea/services/context"
+	"code.gitea.io/gitea/services/convert"
 
+	"github.com/nektos/act/pkg/jobparser"
+	"github.com/nektos/act/pkg/model"
 	"xorm.io/builder"
 )
 
@@ -745,3 +753,164 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
 		url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
 	ctx.JSONRedirect(redirectURL)
 }
+
+func Run(ctx *context_module.Context) {
+	redirectURL := fmt.Sprintf("%s/actions?workflow=%s&actor=%s&status=%s", ctx.Repo.RepoLink, url.QueryEscape(ctx.FormString("workflow")),
+		url.QueryEscape(ctx.FormString("actor")), url.QueryEscape(ctx.FormString("status")))
+
+	workflowID := ctx.FormString("workflow")
+	if len(workflowID) == 0 {
+		ctx.ServerError("workflow", nil)
+		return
+	}
+
+	ref := ctx.FormString("ref")
+	if len(ref) == 0 {
+		ctx.ServerError("ref", nil)
+		return
+	}
+
+	// can not rerun job when workflow is disabled
+	cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
+	cfg := cfgUnit.ActionsConfig()
+	if cfg.IsWorkflowDisabled(workflowID) {
+		ctx.Flash.Error(ctx.Tr("actions.workflow.disabled"))
+		ctx.Redirect(redirectURL)
+		return
+	}
+
+	// get target commit of run from specified ref
+	refName := git.RefName(ref)
+	var runTargetCommit *git.Commit
+	var err error
+	if refName.IsTag() {
+		runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName())
+	} else if refName.IsBranch() {
+		runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName())
+	} else {
+		ctx.Flash.Error(ctx.Tr("form.git_ref_name_error", ref))
+		ctx.Redirect(redirectURL)
+		return
+	}
+	if err != nil {
+		ctx.Flash.Error(ctx.Tr("form.target_ref_not_exist", ref))
+		ctx.Redirect(redirectURL)
+		return
+	}
+
+	// get workflow entry from default branch commit
+	defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+	entries, err := actions.ListWorkflows(defaultBranchCommit)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, err.Error())
+		return
+	}
+
+	// find workflow from commit
+	var workflows []*jobparser.SingleWorkflow
+	for _, entry := range entries {
+		if entry.Name() == workflowID {
+			content, err := actions.GetContentFromEntry(entry)
+			if err != nil {
+				ctx.Error(http.StatusInternalServerError, err.Error())
+				return
+			}
+			workflows, err = jobparser.Parse(content)
+			if err != nil {
+				ctx.ServerError("workflow", err)
+				return
+			}
+			break
+		}
+	}
+
+	if len(workflows) == 0 {
+		ctx.Flash.Error(ctx.Tr("actions.workflow.not_found", workflowID))
+		ctx.Redirect(redirectURL)
+		return
+	}
+
+	// get inputs from post
+	workflow := &model.Workflow{
+		RawOn: workflows[0].RawOn,
+	}
+	inputs := make(map[string]any)
+	if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
+		for name, config := range workflowDispatch.Inputs {
+			value := ctx.Req.PostForm.Get(name)
+			if config.Type == "boolean" {
+				// https://www.w3.org/TR/html401/interact/forms.html
+				// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
+				// Checkboxes (and radio buttons) are on/off switches that may be toggled by the user.
+				// A switch is "on" when the control element's checked attribute is set.
+				// When a form is submitted, only "on" checkbox controls can become successful.
+				inputs[name] = strconv.FormatBool(value == "on")
+			} else if value != "" {
+				inputs[name] = value
+			} else {
+				inputs[name] = config.Default
+			}
+		}
+	}
+
+	// ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
+	// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
+	// https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch
+	workflowDispatchPayload := &api.WorkflowDispatchPayload{
+		Workflow:   workflowID,
+		Ref:        ref,
+		Repository: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}),
+		Inputs:     inputs,
+		Sender:     convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
+	}
+	var eventPayload []byte
+	if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
+		ctx.ServerError("JSONPayload", err)
+		return
+	}
+
+	run := &actions_model.ActionRun{
+		Title:             strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
+		RepoID:            ctx.Repo.Repository.ID,
+		OwnerID:           ctx.Repo.Repository.OwnerID,
+		WorkflowID:        workflowID,
+		TriggerUserID:     ctx.Doer.ID,
+		Ref:               ref,
+		CommitSHA:         runTargetCommit.ID.String(),
+		IsForkPullRequest: false,
+		Event:             "workflow_dispatch",
+		TriggerEvent:      "workflow_dispatch",
+		EventPayload:      string(eventPayload),
+		Status:            actions_model.StatusWaiting,
+	}
+
+	// cancel running jobs of the same workflow
+	if err := actions_model.CancelPreviousJobs(
+		ctx,
+		run.RepoID,
+		run.Ref,
+		run.WorkflowID,
+		run.Event,
+	); err != nil {
+		log.Error("CancelRunningJobs: %v", err)
+	}
+
+	// Insert the action run and its associated jobs into the database
+	if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
+		ctx.ServerError("workflow", err)
+		return
+	}
+
+	alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
+	if err != nil {
+		log.Error("FindRunJobs: %v", err)
+	}
+	actions_service.CreateCommitStatus(ctx, alljobs...)
+
+	ctx.Flash.Success(ctx.Tr("actions.workflow.run_success", workflowID))
+	ctx.Redirect(redirectURL)
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 0e16c1ca6c..4e917b5ede 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1384,6 +1384,7 @@ func registerRoutes(m *web.Router) {
 		m.Get("", actions.List)
 		m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
 		m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
+		m.Post("/run", reqRepoAdmin, actions.Run)
 
 		m.Group("/runs/{run}", func() {
 			m.Combo("").
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index b66d0e360a..7d782c0ade 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -76,6 +76,11 @@
 						</button>
 					{{end}}
 				</div>
+
+				{{if .WorkflowDispatchConfig}}
+					{{template "repo/actions/workflow_dispatch" .}}
+				{{end}}
+
 				{{template "repo/actions/runs_list" .}}
 			</div>
 		</div>
diff --git a/templates/repo/actions/workflow_dispatch.tmpl b/templates/repo/actions/workflow_dispatch.tmpl
new file mode 100644
index 0000000000..21f3ef2077
--- /dev/null
+++ b/templates/repo/actions/workflow_dispatch.tmpl
@@ -0,0 +1,78 @@
+<div class="ui blue info message tw-flex tw-justify-between tw-items-center">
+	<span class="ui text middle">{{ctx.Locale.Tr "actions.workflow.has_workflow_dispatch"}}</span>
+	<button class="ui mini button show-modal" data-modal="#runWorkflowDispatchModal">{{ctx.Locale.Tr "actions.workflow.run"}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}</button>
+</div>
+<div id="runWorkflowDispatchModal" class="ui tiny modal">
+	<div class="content">
+		<form id="runWorkflowDispatchForm" class="ui form" action="{{$.Link}}/run?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}" method="post">
+			{{.CsrfTokenHtml}}
+			<div class="ui inline field required tw-flex tw-items-center">
+				<span class="ui inline required field">
+					<label>{{ctx.Locale.Tr "actions.workflow.from_ref"}}:</label>
+				</span>
+				<div class="ui inline field dropdown button select-branch branch-selector-dropdown ellipsis-items-nowrap">
+					<input type="hidden" name="ref" value="refs/heads/{{index .Branches 0}}">
+					{{svg "octicon-git-branch" 14}}
+					<div class="default text">{{index .Branches 0}}</div>
+					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+					<div class="menu transition">
+						<div class="ui icon search input">
+							<i class="icon">{{svg "octicon-filter" 16}}</i>
+							<input name="search" type="text" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}...">
+						</div>
+						<div class="branch-tag-tab">
+							<a class="branch-tag-item reference column muted active" href="#" data-target="#branch-list">
+								{{svg "octicon-git-branch" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.branches"}}
+							</a>
+							<a class="branch-tag-item reference column muted" href="#" data-target="#tag-list">
+								{{svg "octicon-tag" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.tags"}}
+							</a>
+						</div>
+						<div class="branch-tag-divider"></div>
+						<div id="branch-list" class="scrolling menu reference-list-menu">
+							{{range .Branches}}
+								<div class="item" data-value="refs/heads/{{.}}" title="{{.}}">{{.}}</div>
+							{{else}}
+								<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
+							{{end}}
+						</div>
+						<div id="tag-list" class="scrolling menu reference-list-menu tw-hidden">
+							{{range .Tags}}
+								<div class="item" data-value="refs/tags/{{.}}" title="{{.}}">{{.}}</div>
+							{{else}}
+								<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
+							{{end}}
+						</div>
+					</div>
+				</div>
+			</div>
+
+			<div class="divider"></div>
+
+			{{range $item := .WorkflowDispatchConfig.Inputs}}
+			<div class="ui field {{if .Required}}required{{end}}">
+				{{if eq .Type "choice"}}
+					<label>{{.Description}}:</label>
+					<select class="ui selection type dropdown" name="{{.Name}}">
+						{{range .Options}}
+						<option value="{{.}}" {{if eq $item.Default .}}selected{{end}} >{{.}}</option>
+						{{end}}
+					</select>
+				{{else if eq .Type "boolean"}}
+					<div class="ui inline checkbox">
+						<label>{{.Description}}</label>
+						<input type="checkbox" name="{{.Name}}" {{if eq .Default "true"}}checked{{end}}>
+					</div>
+				{{else if eq .Type "number"}}
+					<label>{{.Description}}:</label>
+					<input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}>
+				{{else}}
+					<label>{{.Description}}:</label>
+					<input name="{{.Name}}" value="{{.Default}}" {{if .Required}}required{{end}}>
+				{{end}}
+			</div>
+			{{end}}
+			<button class="ui tiny primary button" type="submit">Submit</button>
+		</form>
+	</div>
+</div>
diff --git a/web_src/js/features/repo-legacy.ts b/web_src/js/features/repo-legacy.ts
index 75b485cc98..5844037770 100644
--- a/web_src/js/features/repo-legacy.ts
+++ b/web_src/js/features/repo-legacy.ts
@@ -43,6 +43,19 @@ function reloadConfirmDraftComment() {
   window.location.reload();
 }
 
+export function initBranchSelectorTabs() {
+  const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
+  if (!elSelectBranch) return;
+
+  $(elSelectBranch).find('.reference.column').on('click', function () {
+    hideElem($(elSelectBranch).find('.scrolling.reference-list-menu'));
+    showElem(this.getAttribute('data-target'));
+    queryElemChildren(this.parentNode, '.branch-tag-item', (el) => el.classList.remove('active'));
+    this.classList.add('active');
+    return false;
+  });
+}
+
 export function initRepoCommentForm() {
   const $commentForm = $('.comment.form');
   if (!$commentForm.length) return;
@@ -81,13 +94,6 @@ export function initRepoCommentForm() {
         elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
       }
     });
-    $selectBranch.find('.reference.column').on('click', function () {
-      hideElem($selectBranch.find('.scrolling.reference-list-menu'));
-      showElem(this.getAttribute('data-target'));
-      queryElemChildren(this.parentNode, '.branch-tag-item', (el) => el.classList.remove('active'));
-      this.classList.add('active');
-      return false;
-    });
   }
 
   initBranchSelector();
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 81b8828dba..2bdc8655fe 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -60,7 +60,7 @@ import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts';
 import {initRepoBranchButton} from './features/repo-branch.ts';
 import {initCommonOrganization} from './features/common-organization.ts';
 import {initRepoWikiForm} from './features/repo-wiki.ts';
-import {initRepoCommentForm, initRepository} from './features/repo-legacy.ts';
+import {initRepoCommentForm, initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts';
 import {initCopyContent} from './features/copycontent.ts';
 import {initCaptcha} from './features/captcha.ts';
 import {initRepositoryActionView} from './components/RepoActionView.vue';
@@ -182,6 +182,7 @@ onDomReady(() => {
     initRepoBranchButton,
     initRepoCodeView,
     initRepoCommentForm,
+    initBranchSelectorTabs,
     initRepoEllipsisButton,
     initRepoDiffCommitBranchesAndTags,
     initRepoEditor,