diff --git a/models/actions/run.go b/models/actions/run.go
index 5f077940c5612..a846960632104 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -165,6 +165,17 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err
 	return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
 }
 
+func (run *ActionRun) GetWorkflowRunEventPayload() (*api.WorkflowRunPayload, error) {
+	if run.Event == webhook_module.HookEventWorkflowRun {
+		var payload api.WorkflowRunPayload
+		if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
+			return nil, err
+		}
+		return &payload, nil
+	}
+	return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
+}
+
 func (run *ActionRun) IsSchedule() bool {
 	return run.ScheduleID > 0
 }
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index 29f7735d09b11..920ebd31384f4 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -73,7 +73,7 @@ func TestWebhook_EventsArray(t *testing.T) {
 		"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
 		"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
 		"pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release",
-		"package", "status", "workflow_job",
+		"package", "status", "workflow_run", "workflow_job",
 	},
 		(&Webhook{
 			HookEvent: &webhook_module.HookEvent{SendEverything: true},
diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go
index a538b6e290bec..170d9bb3dc66d 100644
--- a/modules/actions/workflows.go
+++ b/modules/actions/workflows.go
@@ -243,6 +243,10 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
 		webhook_module.HookEventPackage:
 		return matchPackageEvent(payload.(*api.PackagePayload), evt)
 
+	case // workflow_run
+		webhook_module.HookEventWorkflowRun:
+		return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt)
+
 	default:
 		log.Warn("unsupported event %q", triggedEvent)
 		return false
@@ -698,3 +702,53 @@ func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool {
 	}
 	return matchTimes == len(evt.Acts())
 }
+
+func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event) bool {
+	// with no special filter parameters
+	if len(evt.Acts()) == 0 {
+		return true
+	}
+
+	matchTimes := 0
+	// all acts conditions should be satisfied
+	for cond, vals := range evt.Acts() {
+		switch cond {
+		case "types":
+			action := payload.Action
+			for _, val := range vals {
+				if glob.MustCompile(val, '/').Match(action) {
+					matchTimes++
+					break
+				}
+			}
+		case "workflows":
+			workflow := payload.Workflow
+			patterns, err := workflowpattern.CompilePatterns(vals...)
+			if err != nil {
+				break
+			}
+			if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) {
+				matchTimes++
+			}
+		case "branches":
+			patterns, err := workflowpattern.CompilePatterns(vals...)
+			if err != nil {
+				break
+			}
+			if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) {
+				matchTimes++
+			}
+		case "branches-ignore":
+			patterns, err := workflowpattern.CompilePatterns(vals...)
+			if err != nil {
+				break
+			}
+			if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) {
+				matchTimes++
+			}
+		default:
+			log.Warn("workflow run event unsupported condition %q", cond)
+		}
+	}
+	return matchTimes == len(evt.Acts())
+}
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index aaa9fbc9d364d..cd0eef851a377 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -470,6 +470,22 @@ func (p *CommitStatusPayload) JSONPayload() ([]byte, error) {
 	return json.MarshalIndent(p, "", "  ")
 }
 
+// WorkflowRunPayload represents a payload information of workflow run event.
+type WorkflowRunPayload struct {
+	Action       string             `json:"action"`
+	Workflow     *ActionWorkflow    `json:"workflow"`
+	WorkflowRun  *ActionWorkflowRun `json:"workflow_run"`
+	PullRequest  *PullRequest       `json:"pull_request,omitempty"`
+	Organization *Organization      `json:"organization,omitempty"`
+	Repo         *Repository        `json:"repository"`
+	Sender       *User              `json:"sender"`
+}
+
+// JSONPayload implements Payload
+func (p *WorkflowRunPayload) JSONPayload() ([]byte, error) {
+	return json.MarshalIndent(p, "", "  ")
+}
+
 // WorkflowJobPayload represents a payload information of workflow job event.
 type WorkflowJobPayload struct {
 	Action       string             `json:"action"`
diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go
index 22409b4aff7fd..eca13825066de 100644
--- a/modules/structs/repo_actions.go
+++ b/modules/structs/repo_actions.go
@@ -86,9 +86,37 @@ type ActionArtifact struct {
 
 // ActionWorkflowRun represents a WorkflowRun
 type ActionWorkflowRun struct {
-	ID           int64  `json:"id"`
-	RepositoryID int64  `json:"repository_id"`
-	HeadSha      string `json:"head_sha"`
+	ID             int64       `json:"id"`
+	URL            string      `json:"url"`
+	HTMLURL        string      `json:"html_url"`
+	DisplayTitle   string      `json:"display_title"`
+	Path           string      `json:"path"`
+	Event          string      `json:"event"`
+	RunAttempt     int64       `json:"run_attempt"`
+	RunNumber      int64       `json:"run_number"`
+	RepositoryID   int64       `json:"repository_id,omitempty"`
+	HeadSha        string      `json:"head_sha"`
+	HeadBranch     string      `json:"head_branch,omitempty"`
+	Status         string      `json:"status"`
+	Repository     *Repository `json:"repository,omitempty"`
+	HeadRepository *Repository `json:"head_repository,omitempty"`
+	Conclusion     string      `json:"conclusion,omitempty"`
+	// swagger:strfmt date-time
+	StartedAt time.Time `json:"started_at,omitempty"`
+	// swagger:strfmt date-time
+	CompletedAt time.Time `json:"completed_at,omitempty"`
+}
+
+// ActionArtifactsResponse returns ActionArtifacts
+type ActionWorkflowRunsResponse struct {
+	Entries    []*ActionWorkflowRun `json:"workflow_runs"`
+	TotalCount int64                `json:"total_count"`
+}
+
+// ActionArtifactsResponse returns ActionArtifacts
+type ActionWorkflowJobsResponse struct {
+	Entries    []*ActionWorkflowJob `json:"jobs"`
+	TotalCount int64                `json:"total_count"`
 }
 
 // ActionArtifactsResponse returns ActionArtifacts
diff --git a/modules/webhook/type.go b/modules/webhook/type.go
index 72ffde26a1574..89c6a4bfe5907 100644
--- a/modules/webhook/type.go
+++ b/modules/webhook/type.go
@@ -38,6 +38,7 @@ const (
 	HookEventPullRequestReview HookEventType = "pull_request_review"
 	// Actions event only
 	HookEventSchedule    HookEventType = "schedule"
+	HookEventWorkflowRun HookEventType = "workflow_run"
 	HookEventWorkflowJob HookEventType = "workflow_job"
 )
 
@@ -67,6 +68,7 @@ func AllEvents() []HookEventType {
 		HookEventRelease,
 		HookEventPackage,
 		HookEventStatus,
+		HookEventWorkflowRun,
 		HookEventWorkflowJob,
 	}
 }
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 96c99615f5d52..5b196a9977991 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2396,6 +2396,8 @@ settings.event_pull_request_review_request_desc = Pull request review requested
 settings.event_pull_request_approvals = Pull Request Approvals
 settings.event_pull_request_merge = Pull Request Merge
 settings.event_header_workflow = Workflow Events
+settings.event_workflow_run = Workflow Run
+settings.event_workflow_run_desc = Gitea Actions Workflow run queued, waiting, in progress, or completed.
 settings.event_workflow_job = Workflow Jobs
 settings.event_workflow_job_desc = Gitea Actions Workflow job queued, waiting, in progress, or completed.
 settings.event_package = Package
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 5cd08a36181e2..38464dcbd6b02 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1247,7 +1247,11 @@ func Routes() *web.Router {
 				}, reqToken(), reqAdmin())
 				m.Group("/actions", func() {
 					m.Get("/tasks", repo.ListActionTasks)
+					m.Get("/runs", repo.GetWorkflowRuns)
+					m.Get("/runs/{run}", repo.GetWorkflowRun)
+					m.Get("/runs/{run}/jobs", repo.GetWorkflowJobs)
 					m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun)
+					m.Get("/jobs/{job_id}", repo.GetWorkflowJob)
 					m.Get("/artifacts", repo.GetArtifacts)
 					m.Group("/artifacts/{artifact_id}", func() {
 						m.Get("", repo.GetArtifact)
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index ed2017a37205c..67705b052bf03 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -21,11 +21,13 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	secret_model "code.gitea.io/gitea/models/secret"
 	"code.gitea.io/gitea/modules/actions"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/modules/webhook"
 	"code.gitea.io/gitea/routers/api/v1/shared"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	actions_service "code.gitea.io/gitea/services/actions"
@@ -637,7 +639,7 @@ func ActionsListRepositoryWorkflows(ctx *context.APIContext) {
 	//   "500":
 	//     "$ref": "#/responses/error"
 
-	workflows, err := actions_service.ListActionWorkflows(ctx)
+	workflows, err := convert.ListActionWorkflows(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository)
 	if err != nil {
 		ctx.APIErrorInternal(err)
 		return
@@ -683,7 +685,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) {
 	//     "$ref": "#/responses/error"
 
 	workflowID := ctx.PathParam("workflow_id")
-	workflow, err := actions_service.GetActionWorkflow(ctx, workflowID)
+	workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID)
 	if err != nil {
 		if errors.Is(err, util.ErrNotExist) {
 			ctx.APIError(http.StatusNotFound, err)
@@ -873,6 +875,262 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
 	ctx.Status(http.StatusNoContent)
 }
 
+func convertToInternal(s string) actions_model.Status {
+	switch s {
+	case "pending":
+		return actions_model.StatusBlocked
+	case "queued":
+		return actions_model.StatusWaiting
+	case "in_progress":
+		return actions_model.StatusRunning
+	case "failure":
+		return actions_model.StatusFailure
+	case "success":
+		return actions_model.StatusSuccess
+	case "skipped":
+		return actions_model.StatusSkipped
+	default:
+		return actions_model.StatusUnknown
+	}
+}
+
+// GetWorkflowRuns Lists all runs for a repository run.
+func GetWorkflowRuns(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns
+	// ---
+	// summary: Lists all runs for a repository run
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: event
+	//   in: query
+	//   description: workflow event name
+	//   type: string
+	//   required: false
+	// - name: branch
+	//   in: query
+	//   description: workflow branch
+	//   type: string
+	//   required: false
+	// - name: status
+	//   in: query
+	//   description: workflow status (pending, queued, in_progress, failure, success, skipped)
+	//   type: string
+	//   required: false
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/ArtifactsList"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	repoID := ctx.Repo.Repository.ID
+
+	opts := actions_model.FindRunOptions{
+		RepoID:      repoID,
+		ListOptions: utils.GetListOptions(ctx),
+	}
+
+	if event := ctx.Req.URL.Query().Get("event"); event != "" {
+		opts.TriggerEvent = webhook.HookEventType(event)
+	}
+	if branch := ctx.Req.URL.Query().Get("branch"); branch != "" {
+		opts.Ref = string(git.RefNameFromBranch(branch))
+	}
+	if status := ctx.Req.URL.Query().Get("status"); status != "" {
+		opts.Status = []actions_model.Status{convertToInternal(status)}
+	}
+	// if actor := ctx.Req.URL.Query().Get("actor"); actor != "" {
+	// 	user_model.
+	// 	opts.TriggerUserID =
+	// }
+
+	runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
+	if err != nil {
+		ctx.APIErrorInternal(err)
+		return
+	}
+
+	res := new(api.ActionWorkflowRunsResponse)
+	res.TotalCount = total
+
+	res.Entries = make([]*api.ActionWorkflowRun, len(runs))
+	for i := range runs {
+		convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, runs[i])
+		if err != nil {
+			ctx.APIErrorInternal(err)
+			return
+		}
+		res.Entries[i] = convertedRun
+	}
+
+	ctx.JSON(http.StatusOK, &res)
+}
+
+// GetWorkflowRun Gets a specific workflow run.
+func GetWorkflowRun(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
+	// ---
+	// summary: Gets a specific workflow run
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: run
+	//   in: path
+	//   description: id of the run
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/Artifact"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	runID := ctx.PathParamInt64("run")
+	job, _, err := db.GetByID[actions_model.ActionRun](ctx, runID)
+
+	if err != nil || job.RepoID != ctx.Repo.Repository.ID {
+		ctx.APIError(http.StatusNotFound, util.ErrNotExist)
+	}
+
+	convertedArtifact, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job)
+	if err != nil {
+		ctx.APIErrorInternal(err)
+		return
+	}
+	ctx.JSON(http.StatusOK, convertedArtifact)
+}
+
+// GetWorkflowJobs Lists all jobs for a workflow run.
+func GetWorkflowJobs(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository getWorkflowJobs
+	// ---
+	// summary: Lists all jobs for a workflow run
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: run
+	//   in: path
+	//   description: runid of the workflow run
+	//   type: integer
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/ArtifactsList"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	repoID := ctx.Repo.Repository.ID
+
+	runID := ctx.PathParamInt64("run")
+
+	artifacts, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
+		RepoID:      repoID,
+		RunID:       runID,
+		ListOptions: utils.GetListOptions(ctx),
+	})
+	if err != nil {
+		ctx.APIErrorInternal(err)
+		return
+	}
+
+	res := new(api.ActionWorkflowJobsResponse)
+	res.TotalCount = total
+
+	res.Entries = make([]*api.ActionWorkflowJob, len(artifacts))
+	for i := range artifacts {
+		convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, artifacts[i])
+		if err != nil {
+			ctx.APIErrorInternal(err)
+			return
+		}
+		res.Entries[i] = convertedWorkflowJob
+	}
+
+	ctx.JSON(http.StatusOK, &res)
+}
+
+// GetWorkflowJob Gets a specific workflow job for a workflow run.
+func GetWorkflowJob(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id} repository getWorkflowJob
+	// ---
+	// summary: Gets a specific workflow job for a workflow run
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: name of the owner
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   type: string
+	//   required: true
+	// - name: job_id
+	//   in: path
+	//   description: id of the job
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/Artifact"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	jobID := ctx.PathParamInt64("job_id")
+	job, _, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID)
+
+	if err != nil || job.RepoID != ctx.Repo.Repository.ID {
+		ctx.APIError(http.StatusNotFound, util.ErrNotExist)
+	}
+
+	convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job)
+	if err != nil {
+		ctx.APIErrorInternal(err)
+		return
+	}
+	ctx.JSON(http.StatusOK, convertedWorkflowJob)
+}
+
 // GetArtifacts Lists all artifacts for a repository.
 func GetArtifactsOfRun(ctx *context.APIContext) {
 	// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 532d157e35df2..9d9d6a960a477 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -206,6 +206,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
 				webhook_module.HookEventRelease:                  util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
 				webhook_module.HookEventPackage:                  util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true),
 				webhook_module.HookEventStatus:                   util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true),
+				webhook_module.HookEventWorkflowRun:              util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowRun), true),
 				webhook_module.HookEventWorkflowJob:              util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowJob), true),
 			},
 			BranchFilter: form.BranchFilter,
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 2ec638926340a..2d81f890088d6 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -459,7 +459,12 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
 	}
 
 	actions_service.CreateCommitStatus(ctx, job)
-	_ = job.LoadAttributes(ctx)
+	// Sync run status with db
+	job.Run = nil
+	if err := job.LoadAttributes(ctx); err != nil {
+		return err
+	}
+	notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
 	notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
 
 	return nil
@@ -531,7 +536,16 @@ func Cancel(ctx *context_module.Context) {
 		_ = job.LoadAttributes(ctx)
 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
 	}
-
+	if len(updatedjobs) > 0 {
+		job := updatedjobs[0]
+		// Sync run status with db
+		job.Run = nil
+		if err := job.LoadAttributes(ctx); err != nil {
+			ctx.HTTPError(http.StatusInternalServerError, err.Error())
+			return
+		}
+		notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+	}
 	ctx.JSON(http.StatusOK, struct{}{})
 }
 
@@ -573,6 +587,14 @@ func Approve(ctx *context_module.Context) {
 
 	actions_service.CreateCommitStatus(ctx, jobs...)
 
+	if len(updatedjobs) > 0 {
+		job := updatedjobs[0]
+		// Sync run status with db
+		job.Run = nil
+		_ = job.LoadAttributes(ctx)
+		notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+	}
+
 	for _, job := range updatedjobs {
 		_ = job.LoadAttributes(ctx)
 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index d3151a86a26fb..006abafe5795d 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -185,6 +185,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
 			webhook_module.HookEventRepository:               form.Repository,
 			webhook_module.HookEventPackage:                  form.Package,
 			webhook_module.HookEventStatus:                   form.Status,
+			webhook_module.HookEventWorkflowRun:              form.WorkflowRun,
 			webhook_module.HookEventWorkflowJob:              form.WorkflowJob,
 		},
 		BranchFilter: form.BranchFilter,
diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go
index 2aeb0e8c96fc6..d74e3f43e2fd5 100644
--- a/services/actions/clear_tasks.go
+++ b/services/actions/clear_tasks.go
@@ -42,6 +42,10 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac
 			_ = job.LoadAttributes(ctx)
 			notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
 		}
+		if len(jobs) > 0 {
+			job := jobs[0]
+			notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+		}
 	}
 }
 
@@ -123,8 +127,11 @@ func CancelAbandonedJobs(ctx context.Context) error {
 		}
 		CreateCommitStatus(ctx, job)
 		if updated {
+			// Sync run status with db
+			job.Run = nil
 			_ = job.LoadAttributes(ctx)
 			notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+			notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
 		}
 	}
 
diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index c11bb5875f45c..e0cf1136f2f0a 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -78,6 +78,24 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
 		_ = job.LoadAttributes(ctx)
 		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
 	}
+	if len(jobs) > 0 {
+		runUpdated := true
+		for _, job := range jobs {
+			if !job.Status.IsDone() {
+				runUpdated = false
+				break
+			}
+		}
+		if runUpdated {
+			// Sync run status with db
+			jobs[0].Run = nil
+			if err := jobs[0].LoadAttributes(ctx); err != nil {
+				return err
+			}
+			run := jobs[0].Run
+			notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
+		}
+	}
 	return nil
 }
 
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 831cde3523f73..1039d48cbda38 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -6,13 +6,16 @@ package actions
 import (
 	"context"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/organization"
 	packages_model "code.gitea.io/gitea/models/packages"
 	perm_model "code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
@@ -762,3 +765,41 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m
 		Sender:       convert.ToUser(ctx, doer, nil),
 	}).Notify(ctx)
 }
+
+func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
+	ctx = withMethod(ctx, "WorkflowRunStatusUpdate")
+
+	var org *api.Organization
+	if repo.Owner.IsOrganization() {
+		org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
+	}
+
+	status := convert.ToWorkflowRunAction(run.Status)
+
+	gitRepo, err := gitrepo.OpenRepository(context.Background(), repo)
+	if err != nil {
+		log.Error("OpenRepository: %v", err)
+		return
+	}
+	defer gitRepo.Close()
+
+	convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
+	if err != nil {
+		log.Error("GetActionWorkflow: %v", err)
+		return
+	}
+	convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run)
+	if err != nil {
+		log.Error("ToActionWorkflowRun: %v", err)
+		return
+	}
+
+	newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{
+		Action:       status,
+		Workflow:     convertedWorkflow,
+		WorkflowRun:  convertedRun,
+		Organization: org,
+		Repo:         convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}),
+		Sender:       convert.ToUser(ctx, sender, nil),
+	}).Notify(ctx)
+}
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index d179134798267..d58229728a9e8 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -178,7 +178,7 @@ func notify(ctx context.Context, input *notifyInput) error {
 		return fmt.Errorf("gitRepo.GetCommit: %w", err)
 	}
 
-	if skipWorkflows(input, commit) {
+	if skipWorkflows(ctx, input, commit) {
 		return nil
 	}
 
@@ -243,7 +243,7 @@ func notify(ctx context.Context, input *notifyInput) error {
 	return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String())
 }
 
-func skipWorkflows(input *notifyInput, commit *git.Commit) bool {
+func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool {
 	// skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync)
 	// https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs
 	skipWorkflowEvents := []webhook_module.HookEventType{
@@ -263,6 +263,24 @@ func skipWorkflows(input *notifyInput, commit *git.Commit) bool {
 			}
 		}
 	}
+	if input.Event == webhook_module.HookEventWorkflowRun {
+		wrun, ok := input.Payload.(*api.WorkflowRunPayload)
+		for i := 0; i < 5 && ok && wrun.WorkflowRun != nil; i++ {
+			if wrun.WorkflowRun.Event != "workflow_run" {
+				return false
+			}
+			r, _ := actions_model.GetRunByID(ctx, wrun.WorkflowRun.ID)
+			var err error
+			wrun, err = r.GetWorkflowRunEventPayload()
+			if err != nil {
+				log.Error("GetWorkflowRunEventPayload: %v", err)
+				return true
+			}
+		}
+		// skip workflow runs events exceeding the maxiumum of 5 recursive events
+		log.Debug("repo %s: skipped workflow_run because of recursive event of 5", input.Repo.RepoPath())
+		return true
+	}
 	return false
 }
 
@@ -364,6 +382,15 @@ func handleWorkflows(
 			continue
 		}
 		CreateCommitStatus(ctx, alljobs...)
+		if len(alljobs) > 0 {
+			job := alljobs[0]
+			err := job.LoadRun(ctx)
+			if err != nil {
+				log.Error("LoadRun: %v", err)
+				continue
+			}
+			notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+		}
 		for _, job := range alljobs {
 			notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil)
 		}
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index a30b1660630bb..c029c5a1a2c8e 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -157,6 +157,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
 	if err != nil {
 		log.Error("LoadAttributes: %v", err)
 	}
+	notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
 	for _, job := range allJobs {
 		notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil)
 	}
diff --git a/services/actions/workflow.go b/services/actions/workflow.go
index dc8a1dd34924f..7f4df0058dd7d 100644
--- a/services/actions/workflow.go
+++ b/services/actions/workflow.go
@@ -5,9 +5,6 @@ package actions
 
 import (
 	"fmt"
-	"net/http"
-	"net/url"
-	"path"
 	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
@@ -31,61 +28,8 @@ import (
 	"github.com/nektos/act/pkg/model"
 )
 
-func getActionWorkflowPath(commit *git.Commit) string {
-	paths := []string{".gitea/workflows", ".github/workflows"}
-	for _, treePath := range paths {
-		if _, err := commit.SubTree(treePath); err == nil {
-			return treePath
-		}
-	}
-	return ""
-}
-
-func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
-	cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
-	cfg := cfgUnit.ActionsConfig()
-
-	defaultBranch, _ := commit.GetBranchName()
-
-	workflowURL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), url.PathEscape(entry.Name()))
-	workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), url.PathEscape(entry.Name()))
-	badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), url.PathEscape(entry.Name()), url.QueryEscape(ctx.Repo.Repository.DefaultBranch))
-
-	// See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow
-	// State types:
-	// - active
-	// - deleted
-	// - disabled_fork
-	// - disabled_inactivity
-	// - disabled_manually
-	state := "active"
-	if cfg.IsWorkflowDisabled(entry.Name()) {
-		state = "disabled_manually"
-	}
-
-	// The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined
-	// by retrieving the first and last commits for the file history. The first commit would indicate the creation date,
-	// while the last commit would represent the modification date. The DeletedAt could be determined by identifying
-	// the last commit where the file existed. However, this implementation has not been done here yet, as it would likely
-	// cause a significant performance degradation.
-	createdAt := commit.Author.When
-	updatedAt := commit.Author.When
-
-	return &api.ActionWorkflow{
-		ID:        entry.Name(),
-		Name:      entry.Name(),
-		Path:      path.Join(folder, entry.Name()),
-		State:     state,
-		CreatedAt: createdAt,
-		UpdatedAt: updatedAt,
-		URL:       workflowURL,
-		HTMLURL:   workflowRepoURL,
-		BadgeURL:  badgeURL,
-	}
-}
-
 func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error {
-	workflow, err := GetActionWorkflow(ctx, workflowID)
+	workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID)
 	if err != nil {
 		return err
 	}
@@ -102,44 +46,6 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl
 	return repo_model.UpdateRepoUnit(ctx, cfgUnit)
 }
 
-func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) {
-	defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
-	if err != nil {
-		ctx.APIErrorInternal(err)
-		return nil, err
-	}
-
-	entries, err := actions.ListWorkflows(defaultBranchCommit)
-	if err != nil {
-		ctx.APIError(http.StatusNotFound, err.Error())
-		return nil, err
-	}
-
-	folder := getActionWorkflowPath(defaultBranchCommit)
-
-	workflows := make([]*api.ActionWorkflow, len(entries))
-	for i, entry := range entries {
-		workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry)
-	}
-
-	return workflows, nil
-}
-
-func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionWorkflow, error) {
-	entries, err := ListActionWorkflows(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	for _, entry := range entries {
-		if entry.Name == workflowID {
-			return entry, nil
-		}
-	}
-
-	return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
-}
-
 func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error {
 	if workflowID == "" {
 		return util.ErrorWrapLocale(
@@ -277,6 +183,15 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
 		log.Error("FindRunJobs: %v", err)
 	}
 	CreateCommitStatus(ctx, allJobs...)
+	if len(allJobs) > 0 {
+		job := allJobs[0]
+		err := job.LoadRun(ctx)
+		if err != nil {
+			log.Error("LoadRun: %v", err)
+		} else {
+			notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+		}
+	}
 	for _, job := range allJobs {
 		notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil)
 	}
diff --git a/services/convert/convert.go b/services/convert/convert.go
index ac2680766c040..0ce6bf15d39ef 100644
--- a/services/convert/convert.go
+++ b/services/convert/convert.go
@@ -7,6 +7,8 @@ package convert
 import (
 	"context"
 	"fmt"
+	"net/url"
+	"path"
 	"strconv"
 	"strings"
 	"time"
@@ -14,6 +16,7 @@ import (
 	actions_model "code.gitea.io/gitea/models/actions"
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
@@ -22,6 +25,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
@@ -230,6 +234,233 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action
 	}, nil
 }
 
+func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) {
+	err := run.LoadRepo(ctx)
+	if err != nil {
+		return nil, err
+	}
+	status, conclusion := ToActionsStatus(run.Status)
+	return &api.ActionWorkflowRun{
+		ID:           run.ID,
+		URL:          fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID),
+		HTMLURL:      run.HTMLURL(),
+		RunNumber:    run.Index,
+		StartedAt:    run.Started.AsLocalTime(),
+		CompletedAt:  run.Stopped.AsLocalTime(),
+		Event:        string(run.Event),
+		DisplayTitle: run.Title,
+		HeadBranch:   git.RefName(run.Ref).BranchName(),
+		HeadSha:      run.CommitSHA,
+		Status:       status,
+		Conclusion:   conclusion,
+		Path:         fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref),
+		Repository:   ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
+	}, nil
+}
+
+func ToWorkflowRunAction(status actions_model.Status) string {
+	var action string
+	switch status {
+	case actions_model.StatusWaiting, actions_model.StatusBlocked:
+		action = "requested"
+	case actions_model.StatusRunning:
+		action = "in_progress"
+	}
+	if status.IsDone() {
+		action = "completed"
+	}
+	return action
+}
+
+func ToActionsStatus(status actions_model.Status) (string, string) {
+	var action string
+	var conclusion string
+	switch status {
+	// This is a naming conflict of the webhook between Gitea and GitHub Actions
+	case actions_model.StatusWaiting:
+		action = "queued"
+	case actions_model.StatusBlocked:
+		action = "waiting"
+	case actions_model.StatusRunning:
+		action = "in_progress"
+	}
+	if status.IsDone() {
+		action = "completed"
+		switch status {
+		case actions_model.StatusSuccess:
+			conclusion = "success"
+		case actions_model.StatusCancelled:
+			conclusion = "cancelled"
+		case actions_model.StatusFailure:
+			conclusion = "failure"
+		}
+	}
+	return action, conclusion
+}
+
+// ToActionWorkflowJob convert a actions_model.ActionRunJob to an api.ActionWorkflowJob
+// task is optional and can be nil
+func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask, job *actions_model.ActionRunJob) (*api.ActionWorkflowJob, error) {
+	err := job.LoadAttributes(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	jobIndex := 0
+	jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
+	if err != nil {
+		return nil, err
+	}
+	for i, j := range jobs {
+		if j.ID == job.ID {
+			jobIndex = i
+			break
+		}
+	}
+
+	status, conclusion := ToActionsStatus(job.Status)
+	var runnerID int64
+	var runnerName string
+	var steps []*api.ActionWorkflowStep
+
+	if job.TaskID != 0 {
+		if task == nil {
+			task, _, err = db.GetByID[actions_model.ActionTask](ctx, job.TaskID)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		runnerID = task.RunnerID
+		if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok {
+			runnerName = runner.Name
+		}
+		for i, step := range task.Steps {
+			stepStatus, stepConclusion := ToActionsStatus(job.Status)
+			steps = append(steps, &api.ActionWorkflowStep{
+				Name:        step.Name,
+				Number:      int64(i),
+				Status:      stepStatus,
+				Conclusion:  stepConclusion,
+				StartedAt:   step.Started.AsTime().UTC(),
+				CompletedAt: step.Stopped.AsTime().UTC(),
+			})
+		}
+	}
+
+	return &api.ActionWorkflowJob{
+		ID: job.ID,
+		// missing api endpoint for this location
+		URL:     fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(), job.ID),
+		HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex),
+		RunID:   job.RunID,
+		// Missing api endpoint for this location, artifacts are available under a nested url
+		RunURL:      fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID),
+		Name:        job.Name,
+		Labels:      job.RunsOn,
+		RunAttempt:  job.Attempt,
+		HeadSha:     job.Run.CommitSHA,
+		HeadBranch:  git.RefName(job.Run.Ref).BranchName(),
+		Status:      status,
+		Conclusion:  conclusion,
+		RunnerID:    runnerID,
+		RunnerName:  runnerName,
+		Steps:       steps,
+		CreatedAt:   job.Created.AsTime().UTC(),
+		StartedAt:   job.Started.AsTime().UTC(),
+		CompletedAt: job.Stopped.AsTime().UTC(),
+	}, nil
+}
+
+func getActionWorkflowPath(commit *git.Commit) string {
+	paths := []string{".gitea/workflows", ".github/workflows"}
+	for _, treePath := range paths {
+		if _, err := commit.SubTree(treePath); err == nil {
+			return treePath
+		}
+	}
+	return ""
+}
+
+func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
+	cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
+	cfg := cfgUnit.ActionsConfig()
+
+	defaultBranch, _ := commit.GetBranchName()
+
+	workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), url.PathEscape(entry.Name()))
+	workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), url.PathEscape(entry.Name()))
+	badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repo.HTMLURL(ctx), url.PathEscape(entry.Name()), url.QueryEscape(repo.DefaultBranch))
+
+	// See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow
+	// State types:
+	// - active
+	// - deleted
+	// - disabled_fork
+	// - disabled_inactivity
+	// - disabled_manually
+	state := "active"
+	if cfg.IsWorkflowDisabled(entry.Name()) {
+		state = "disabled_manually"
+	}
+
+	// The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined
+	// by retrieving the first and last commits for the file history. The first commit would indicate the creation date,
+	// while the last commit would represent the modification date. The DeletedAt could be determined by identifying
+	// the last commit where the file existed. However, this implementation has not been done here yet, as it would likely
+	// cause a significant performance degradation.
+	createdAt := commit.Author.When
+	updatedAt := commit.Author.When
+
+	return &api.ActionWorkflow{
+		ID:        entry.Name(),
+		Name:      entry.Name(),
+		Path:      path.Join(folder, entry.Name()),
+		State:     state,
+		CreatedAt: createdAt,
+		UpdatedAt: updatedAt,
+		URL:       workflowURL,
+		HTMLURL:   workflowRepoURL,
+		BadgeURL:  badgeURL,
+	}
+}
+
+func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository) ([]*api.ActionWorkflow, error) {
+	defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch)
+	if err != nil {
+		return nil, err
+	}
+
+	entries, err := actions.ListWorkflows(defaultBranchCommit)
+	if err != nil {
+		return nil, err
+	}
+
+	folder := getActionWorkflowPath(defaultBranchCommit)
+
+	workflows := make([]*api.ActionWorkflow, len(entries))
+	for i, entry := range entries {
+		workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, folder, entry)
+	}
+
+	return workflows, nil
+}
+
+func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) {
+	entries, err := ListActionWorkflows(ctx, gitrepo, repo)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, entry := range entries {
+		if entry.Name == workflowID {
+			return entry, nil
+		}
+	}
+
+	return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
+}
+
 // ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact
 func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) {
 	url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID)
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index d20220b7847a6..a54f3aac15612 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -234,6 +234,7 @@ type WebhookForm struct {
 	Release                  bool
 	Package                  bool
 	Status                   bool
+	WorkflowRun              bool
 	WorkflowJob              bool
 	Active                   bool
 	BranchFilter             string `binding:"GlobPattern"`
diff --git a/services/notify/notifier.go b/services/notify/notifier.go
index 40428454be0af..875a70e5644a7 100644
--- a/services/notify/notifier.go
+++ b/services/notify/notifier.go
@@ -79,5 +79,7 @@ type Notifier interface {
 
 	CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus)
 
+	WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun)
+
 	WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask)
 }
diff --git a/services/notify/notify.go b/services/notify/notify.go
index 9f8be4b577373..0c6fdf9cef9df 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -376,6 +376,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit
 	}
 }
 
+func WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
+	for _, notifier := range notifiers {
+		notifier.WorkflowRunStatusUpdate(ctx, repo, sender, run)
+	}
+}
+
 func WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
 	for _, notifier := range notifiers {
 		notifier.WorkflowJobStatusUpdate(ctx, repo, sender, job, task)
diff --git a/services/notify/null.go b/services/notify/null.go
index 9c794a2342cf7..c3085d7c9eb0a 100644
--- a/services/notify/null.go
+++ b/services/notify/null.go
@@ -214,5 +214,8 @@ func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.R
 func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
 }
 
+func (*NullNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
+}
+
 func (*NullNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
 }
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index ce907bf0cb786..5bbc610fe5c7b 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -176,6 +176,12 @@ func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload,
 	return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
 }
 
+func (dingtalkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DingtalkPayload, error) {
+	text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+	return createDingtalkPayload(text, text, "Workflow Run", p.WorkflowRun.HTMLURL), nil
+}
+
 func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) {
 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
 
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 0e8a9aa67c87c..b3637acb17230 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -278,6 +278,12 @@ func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, er
 	return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
 }
 
+func (d discordConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DiscordPayload, error) {
+	text, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
+
+	return d.createPayload(p.Sender, text, "", p.WorkflowRun.HTMLURL, color), nil
+}
+
 func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) {
 	text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
 
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 274aaf90b3b28..c7d2309ac4a5e 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -172,6 +172,12 @@ func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, err
 	return newFeishuTextPayload(text), nil
 }
 
+func (feishuConvertor) WorkflowRun(p *api.WorkflowRunPayload) (FeishuPayload, error) {
+	text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+	return newFeishuTextPayload(text), nil
+}
+
 func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) {
 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
 
diff --git a/services/webhook/general.go b/services/webhook/general.go
index 251659e75ebf3..bcb8eb14a0d34 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -327,6 +327,37 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte
 	return text, color
 }
 
+func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+	description := p.WorkflowRun.Conclusion
+	if description == "" {
+		description = p.WorkflowRun.Status
+	}
+	refLink := linkFormatter(p.WorkflowRun.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowRun.DisplayTitle, p.WorkflowRun.ID)+"["+base.ShortSha(p.WorkflowRun.HeadSha)+"]:"+description)
+
+	text = fmt.Sprintf("Workflow Run %s: %s", p.Action, refLink)
+	switch description {
+	case "waiting":
+		color = orangeColor
+	case "queued":
+		color = orangeColorLight
+	case "success":
+		color = greenColor
+	case "failure":
+		color = redColor
+	case "cancelled":
+		color = yellowColor
+	case "skipped":
+		color = purpleColor
+	default:
+		color = greyColor
+	}
+	if withSender {
+		text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+	}
+
+	return text, color
+}
+
 func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
 	description := p.WorkflowJob.Conclusion
 	if description == "" {
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 5bc7ba097e42a..3e9163f78c2f2 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -252,6 +252,12 @@ func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, erro
 	return m.newPayload(text)
 }
 
+func (m matrixConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MatrixPayload, error) {
+	text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
+
+	return m.newPayload(text)
+}
+
 func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) {
 	text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
 
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 07d28c3867462..450a544b42ee4 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -318,6 +318,20 @@ func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, er
 	), nil
 }
 
+func (msteamsConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MSTeamsPayload, error) {
+	title, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
+
+	return createMSTeamsPayload(
+		p.Repo,
+		p.Sender,
+		title,
+		"",
+		p.WorkflowRun.HTMLURL,
+		color,
+		&MSTeamsFact{"WorkflowRun:", p.WorkflowRun.DisplayTitle},
+	), nil
+}
+
 func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) {
 	title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
 
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 9e3f21de290ee..dc44460860081 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -5,10 +5,8 @@ package webhook
 
 import (
 	"context"
-	"fmt"
 
 	actions_model "code.gitea.io/gitea/models/actions"
-	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
@@ -18,6 +16,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/repository"
@@ -956,72 +955,61 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_
 		org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
 	}
 
-	err := job.LoadAttributes(ctx)
+	status, _ := convert.ToActionsStatus(job.Status)
+
+	convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job)
 	if err != nil {
-		log.Error("Error loading job attributes: %v", err)
+		log.Error("ToActionWorkflowJob: %v", err)
 		return
 	}
 
-	jobIndex := 0
-	jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
+	if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{
+		Action:       status,
+		WorkflowJob:  convertedJob,
+		Organization: org,
+		Repo:         convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
+		Sender:       convert.ToUser(ctx, sender, nil),
+	}); err != nil {
+		log.Error("PrepareWebhooks: %v", err)
+	}
+}
+
+func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) {
+	source := EventSource{
+		Repository: repo,
+		Owner:      repo.Owner,
+	}
+
+	var org *api.Organization
+	if repo.Owner.IsOrganization() {
+		org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
+	}
+
+	status := convert.ToWorkflowRunAction(run.Status)
+
+	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
 	if err != nil {
-		log.Error("Error loading getting run jobs: %v", err)
+		log.Error("OpenRepository: %v", err)
 		return
 	}
-	for i, j := range jobs {
-		if j.ID == job.ID {
-			jobIndex = i
-			break
-		}
-	}
+	defer gitRepo.Close()
 
-	status, conclusion := toActionStatus(job.Status)
-	var runnerID int64
-	var runnerName string
-	var steps []*api.ActionWorkflowStep
+	convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID)
+	if err != nil {
+		log.Error("GetActionWorkflow: %v", err)
+		return
+	}
 
-	if task != nil {
-		runnerID = task.RunnerID
-		if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok {
-			runnerName = runner.Name
-		}
-		for i, step := range task.Steps {
-			stepStatus, stepConclusion := toActionStatus(job.Status)
-			steps = append(steps, &api.ActionWorkflowStep{
-				Name:        step.Name,
-				Number:      int64(i),
-				Status:      stepStatus,
-				Conclusion:  stepConclusion,
-				StartedAt:   step.Started.AsTime().UTC(),
-				CompletedAt: step.Stopped.AsTime().UTC(),
-			})
-		}
+	convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run)
+	if err != nil {
+		log.Error("ToActionWorkflowRun: %v", err)
+		return
 	}
 
-	if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{
-		Action: status,
-		WorkflowJob: &api.ActionWorkflowJob{
-			ID: job.ID,
-			// missing api endpoint for this location
-			URL:     fmt.Sprintf("%s/actions/runs/%d/jobs/%d", repo.APIURL(), job.RunID, job.ID),
-			HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex),
-			RunID:   job.RunID,
-			// Missing api endpoint for this location, artifacts are available under a nested url
-			RunURL:      fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID),
-			Name:        job.Name,
-			Labels:      job.RunsOn,
-			RunAttempt:  job.Attempt,
-			HeadSha:     job.Run.CommitSHA,
-			HeadBranch:  git.RefName(job.Run.Ref).BranchName(),
-			Status:      status,
-			Conclusion:  conclusion,
-			RunnerID:    runnerID,
-			RunnerName:  runnerName,
-			Steps:       steps,
-			CreatedAt:   job.Created.AsTime().UTC(),
-			StartedAt:   job.Started.AsTime().UTC(),
-			CompletedAt: job.Stopped.AsTime().UTC(),
-		},
+	if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowRun, &api.WorkflowRunPayload{
+		Action:       status,
+		Workflow:     convertedWorkflow,
+		WorkflowRun:  convertedRun,
 		Organization: org,
 		Repo:         convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
 		Sender:       convert.ToUser(ctx, sender, nil),
@@ -1029,29 +1017,3 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_
 		log.Error("PrepareWebhooks: %v", err)
 	}
 }
-
-func toActionStatus(status actions_model.Status) (string, string) {
-	var action string
-	var conclusion string
-	switch status {
-	// This is a naming conflict of the webhook between Gitea and GitHub Actions
-	case actions_model.StatusWaiting:
-		action = "queued"
-	case actions_model.StatusBlocked:
-		action = "waiting"
-	case actions_model.StatusRunning:
-		action = "in_progress"
-	}
-	if status.IsDone() {
-		action = "completed"
-		switch status {
-		case actions_model.StatusSuccess:
-			conclusion = "success"
-		case actions_model.StatusCancelled:
-			conclusion = "cancelled"
-		case actions_model.StatusFailure:
-			conclusion = "failure"
-		}
-	}
-	return action, conclusion
-}
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 8829d95da606a..e6a00b0293364 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -114,6 +114,10 @@ func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayloa
 	return PackagistPayload{}, nil
 }
 
+func (pc packagistConvertor) WorkflowRun(_ *api.WorkflowRunPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
+}
+
 func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) {
 	return PackagistPayload{}, nil
 }
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index adb7243fb14ba..c25d700c231d0 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -29,6 +29,7 @@ type payloadConvertor[T any] interface {
 	Wiki(*api.WikiPayload) (T, error)
 	Package(*api.PackagePayload) (T, error)
 	Status(*api.CommitStatusPayload) (T, error)
+	WorkflowRun(*api.WorkflowRunPayload) (T, error)
 	WorkflowJob(*api.WorkflowJobPayload) (T, error)
 }
 
@@ -81,6 +82,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
 		return convertUnmarshalledJSON(rc.Package, data)
 	case webhook_module.HookEventStatus:
 		return convertUnmarshalledJSON(rc.Status, data)
+	case webhook_module.HookEventWorkflowRun:
+		return convertUnmarshalledJSON(rc.WorkflowRun, data)
 	case webhook_module.HookEventWorkflowJob:
 		return convertUnmarshalledJSON(rc.WorkflowJob, data)
 	}
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 589ef3fe9bd68..3d645a55d0441 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -173,6 +173,12 @@ func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error)
 	return s.createPayload(text, nil), nil
 }
 
+func (s slackConvertor) WorkflowRun(p *api.WorkflowRunPayload) (SlackPayload, error) {
+	text, _ := getWorkflowRunPayloadInfo(p, SlackLinkFormatter, true)
+
+	return s.createPayload(text, nil), nil
+}
+
 func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) {
 	text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true)
 
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index ca74eabe1c4e9..ae195758b9721 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -180,6 +180,12 @@ func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload,
 	return createTelegramPayloadHTML(text), nil
 }
 
+func (telegramConvertor) WorkflowRun(p *api.WorkflowRunPayload) (TelegramPayload, error) {
+	text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true)
+
+	return createTelegramPayloadHTML(text), nil
+}
+
 func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) {
 	text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
 
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 2b19822caf6f6..187531740658b 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -181,6 +181,12 @@ func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayl
 	return newWechatworkMarkdownPayload(text), nil
 }
 
+func (wc wechatworkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (WechatworkPayload, error) {
+	text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
+
+	return newWechatworkMarkdownPayload(text), nil
+}
+
 func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) {
 	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
 
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index 16ad263e42a58..b8d9609391ff0 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -263,6 +263,16 @@
 		<div class="fourteen wide column">
 			<label>{{ctx.Locale.Tr "repo.settings.event_header_workflow"}}</label>
 		</div>
+		<!-- Workflow Run Event -->
+		<div class="seven wide column">
+			<div class="field">
+				<div class="ui checkbox">
+					<input name="workflow_run" type="checkbox" {{if .Webhook.HookEvents.Get "workflow_run"}}checked{{end}}>
+					<label>{{ctx.Locale.Tr "repo.settings.event_workflow_run"}}</label>
+					<span class="help">{{ctx.Locale.Tr "repo.settings.event_workflow_run_desc"}}</span>
+				</div>
+			</div>
+		</div>
 		<!-- Workflow Job Event -->
 		<div class="seven wide column">
 			<div class="field">
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index d0e41e8094ea4..551d41e37078f 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -4187,6 +4187,52 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/actions/jobs/{job_id}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Gets a specific workflow job for a workflow run",
+        "operationId": "getWorkflowJob",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "id of the job",
+            "name": "job_id",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/Artifact"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/actions/jobs/{job_id}/logs": {
       "get": {
         "produces": [
@@ -4266,6 +4312,109 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/actions/runs": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Lists all runs for a repository run",
+        "operationId": "getWorkflowRuns",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "workflow event name",
+            "name": "event",
+            "in": "query"
+          },
+          {
+            "type": "string",
+            "description": "workflow branch",
+            "name": "branch",
+            "in": "query"
+          },
+          {
+            "type": "string",
+            "description": "workflow status (pending, queued, in_progress, failure, success, skipped)",
+            "name": "status",
+            "in": "query"
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ArtifactsList"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
+    "/repos/{owner}/{repo}/actions/runs/{run}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Gets a specific workflow run",
+        "operationId": "GetWorkflowRun",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "id of the run",
+            "name": "run",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/Artifact"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/actions/runs/{run}/artifacts": {
       "get": {
         "produces": [
@@ -4318,6 +4467,52 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/actions/runs/{run}/jobs": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Lists all jobs for a workflow run",
+        "operationId": "getWorkflowJobs",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "name of the owner",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "runid of the workflow run",
+            "name": "run",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/ArtifactsList"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/actions/secrets": {
       "get": {
         "produces": [
@@ -19450,19 +19645,77 @@
       "description": "ActionWorkflowRun represents a WorkflowRun",
       "type": "object",
       "properties": {
+        "completed_at": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "CompletedAt"
+        },
+        "conclusion": {
+          "type": "string",
+          "x-go-name": "Conclusion"
+        },
+        "display_title": {
+          "type": "string",
+          "x-go-name": "DisplayTitle"
+        },
+        "event": {
+          "type": "string",
+          "x-go-name": "Event"
+        },
+        "head_branch": {
+          "type": "string",
+          "x-go-name": "HeadBranch"
+        },
+        "head_repository": {
+          "$ref": "#/definitions/Repository"
+        },
         "head_sha": {
           "type": "string",
           "x-go-name": "HeadSha"
         },
+        "html_url": {
+          "type": "string",
+          "x-go-name": "HTMLURL"
+        },
         "id": {
           "type": "integer",
           "format": "int64",
           "x-go-name": "ID"
         },
+        "path": {
+          "type": "string",
+          "x-go-name": "Path"
+        },
+        "repository": {
+          "$ref": "#/definitions/Repository"
+        },
         "repository_id": {
           "type": "integer",
           "format": "int64",
           "x-go-name": "RepositoryID"
+        },
+        "run_attempt": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "RunAttempt"
+        },
+        "run_number": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "RunNumber"
+        },
+        "started_at": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "StartedAt"
+        },
+        "status": {
+          "type": "string",
+          "x-go-name": "Status"
+        },
+        "url": {
+          "type": "string",
+          "x-go-name": "URL"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
index 89df15b8de8c3..6d8f9a790aeac 100644
--- a/tests/integration/repo_webhook_test.go
+++ b/tests/integration/repo_webhook_test.go
@@ -707,8 +707,7 @@ jobs:
 		assert.Equal(t, commitID, payloads[3].WorkflowJob.HeadSha)
 		assert.Equal(t, "repo1", payloads[3].Repo.Name)
 		assert.Equal(t, "user2/repo1", payloads[3].Repo.FullName)
-		assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/runs/%d/jobs/%d", payloads[3].WorkflowJob.RunID, payloads[3].WorkflowJob.ID))
-		assert.Contains(t, payloads[3].WorkflowJob.URL, payloads[3].WorkflowJob.RunURL)
+		assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[3].WorkflowJob.ID))
 		assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 0))
 		assert.Len(t, payloads[3].WorkflowJob.Steps, 1)
 
@@ -745,8 +744,7 @@ jobs:
 		assert.Equal(t, commitID, payloads[6].WorkflowJob.HeadSha)
 		assert.Equal(t, "repo1", payloads[6].Repo.Name)
 		assert.Equal(t, "user2/repo1", payloads[6].Repo.FullName)
-		assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/runs/%d/jobs/%d", payloads[6].WorkflowJob.RunID, payloads[6].WorkflowJob.ID))
-		assert.Contains(t, payloads[6].WorkflowJob.URL, payloads[6].WorkflowJob.RunURL)
+		assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[6].WorkflowJob.ID))
 		assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 1))
 		assert.Len(t, payloads[6].WorkflowJob.Steps, 2)
 	})
diff --git a/tests/integration/workflow_run_api_check_test.go b/tests/integration/workflow_run_api_check_test.go
new file mode 100644
index 0000000000000..f142da7b226af
--- /dev/null
+++ b/tests/integration/workflow_run_api_check_test.go
@@ -0,0 +1,72 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	auth_model "code.gitea.io/gitea/models/auth"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAPIWorkflowRunRepoApi(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	userUsername := "user5"
+	token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
+
+	req := NewRequest(t, "GET", "/api/v1/repos/user5/repo4/actions/runs").AddTokenAuth(token)
+	runnerListResp := MakeRequest(t, req, http.StatusOK)
+	runnerList := api.ActionWorkflowRunsResponse{}
+	DecodeJSON(t, runnerListResp, &runnerList)
+
+	assert.Len(t, runnerList.Entries, 4)
+
+	for _, run := range runnerList.Entries {
+		req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", run.URL, "jobs")).AddTokenAuth(token)
+		jobsResp := MakeRequest(t, req, http.StatusOK)
+		jobList := api.ActionWorkflowJobsResponse{}
+		DecodeJSON(t, jobsResp, &jobList)
+
+		// assert.NotEmpty(t, jobList.Entries)
+		for _, job := range jobList.Entries {
+			req := NewRequest(t, "GET", job.URL).AddTokenAuth(token)
+			jobsResp := MakeRequest(t, req, http.StatusOK)
+			apiJob := api.ActionWorkflowJob{}
+			DecodeJSON(t, jobsResp, &apiJob)
+			assert.Equal(t, job.ID, apiJob.ID)
+			assert.Equal(t, job.RunID, apiJob.RunID)
+			assert.Equal(t, job.Status, apiJob.Status)
+			assert.Equal(t, job.Conclusion, apiJob.Conclusion)
+		}
+		// assert.NotEmpty(t, run.ID)
+		// assert.NotEmpty(t, run.Status)
+		// assert.NotEmpty(t, run.Event)
+		// assert.NotEmpty(t, run.WorkflowID)
+		// assert.NotEmpty(t, run.HeadBranch)
+		// assert.NotEmpty(t, run.HeadSHA)
+		// assert.NotEmpty(t, run.CreatedAt)
+		// assert.NotEmpty(t, run.UpdatedAt)
+		// assert.NotEmpty(t, run.URL)
+		// assert.NotEmpty(t, run.HTMLURL)
+		// assert.NotEmpty(t, run.PullRequests)
+		// assert.NotEmpty(t, run.WorkflowURL)
+		// assert.NotEmpty(t, run.HeadCommit)
+		// assert.NotEmpty(t, run.HeadRepository)
+		// assert.NotEmpty(t, run.Repository)
+		// assert.NotEmpty(t, run.HeadRepository)
+		// assert.NotEmpty(t, run.HeadRepository.Owner)
+		// assert.NotEmpty(t, run.HeadRepository.Name)
+		// assert.NotEmpty(t, run.Repository.Owner)
+		// assert.NotEmpty(t, run.Repository.Name)
+		// assert.NotEmpty(t, run.HeadRepository.Owner.Login)
+		// assert.NotEmpty(t, run.HeadRepository.Name)
+		// assert.NotEmpty(t, run.Repository.Owner.Login)
+		// assert.NotEmpty(t, run.Repository.Name)
+	}
+}