diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index f2a26efbb9..6ff77a380d 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",
+		"package", "status", "workflow_job",
 	},
 		(&Webhook{
 			HookEvent: &webhook_module.HookEvent{SendEverything: true},
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index cef2dbd712..aaa9fbc9d3 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -469,3 +469,18 @@ type CommitStatusPayload struct {
 func (p *CommitStatusPayload) JSONPayload() ([]byte, error) {
 	return json.MarshalIndent(p, "", "  ")
 }
+
+// WorkflowJobPayload represents a payload information of workflow job event.
+type WorkflowJobPayload struct {
+	Action       string             `json:"action"`
+	WorkflowJob  *ActionWorkflowJob `json:"workflow_job"`
+	PullRequest  *PullRequest       `json:"pull_request,omitempty"`
+	Organization *Organization      `json:"organization,omitempty"`
+	Repo         *Repository        `json:"repository"`
+	Sender       *User              `json:"sender"`
+}
+
+// JSONPayload implements Payload
+func (p *WorkflowJobPayload) JSONPayload() ([]byte, error) {
+	return json.MarshalIndent(p, "", "  ")
+}
diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go
index 203491ac02..22409b4aff 100644
--- a/modules/structs/repo_actions.go
+++ b/modules/structs/repo_actions.go
@@ -96,3 +96,40 @@ type ActionArtifactsResponse struct {
 	Entries    []*ActionArtifact `json:"artifacts"`
 	TotalCount int64             `json:"total_count"`
 }
+
+// ActionWorkflowStep represents a step of a WorkflowJob
+type ActionWorkflowStep struct {
+	Name       string `json:"name"`
+	Number     int64  `json:"number"`
+	Status     string `json:"status"`
+	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"`
+}
+
+// ActionWorkflowJob represents a WorkflowJob
+type ActionWorkflowJob struct {
+	ID         int64                 `json:"id"`
+	URL        string                `json:"url"`
+	HTMLURL    string                `json:"html_url"`
+	RunID      int64                 `json:"run_id"`
+	RunURL     string                `json:"run_url"`
+	Name       string                `json:"name"`
+	Labels     []string              `json:"labels"`
+	RunAttempt int64                 `json:"run_attempt"`
+	HeadSha    string                `json:"head_sha"`
+	HeadBranch string                `json:"head_branch,omitempty"`
+	Status     string                `json:"status"`
+	Conclusion string                `json:"conclusion,omitempty"`
+	RunnerID   int64                 `json:"runner_id,omitempty"`
+	RunnerName string                `json:"runner_name,omitempty"`
+	Steps      []*ActionWorkflowStep `json:"steps"`
+	// swagger:strfmt date-time
+	CreatedAt time.Time `json:"created_at"`
+	// swagger:strfmt date-time
+	StartedAt time.Time `json:"started_at,omitempty"`
+	// swagger:strfmt date-time
+	CompletedAt time.Time `json:"completed_at,omitempty"`
+}
diff --git a/modules/webhook/type.go b/modules/webhook/type.go
index b244bb0cff..72ffde26a1 100644
--- a/modules/webhook/type.go
+++ b/modules/webhook/type.go
@@ -37,7 +37,8 @@ const (
 	// FIXME: This event should be a group of pull_request_review_xxx events
 	HookEventPullRequestReview HookEventType = "pull_request_review"
 	// Actions event only
-	HookEventSchedule HookEventType = "schedule"
+	HookEventSchedule    HookEventType = "schedule"
+	HookEventWorkflowJob HookEventType = "workflow_job"
 )
 
 func AllEvents() []HookEventType {
@@ -66,6 +67,7 @@ func AllEvents() []HookEventType {
 		HookEventRelease,
 		HookEventPackage,
 		HookEventStatus,
+		HookEventWorkflowJob,
 	}
 }
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 4f1db5da6a..2f13c1a19c 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2380,6 +2380,9 @@ settings.event_pull_request_review_request = Pull Request Review Requested
 settings.event_pull_request_review_request_desc = Pull request review requested or review request removed.
 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_job = Workflow Jobs
+settings.event_workflow_job_desc = Gitea Actions Workflow job queued, waiting, in progress, or completed.
 settings.event_package = Package
 settings.event_package_desc = Package created or deleted in a repository.
 settings.branch_filter = Branch filter
diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go
index f34dfb443b..27a0317942 100644
--- a/routers/api/actions/runner/runner.go
+++ b/routers/api/actions/runner/runner.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
 	actions_service "code.gitea.io/gitea/services/actions"
+	notify_service "code.gitea.io/gitea/services/notify"
 
 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
 	"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
@@ -210,7 +211,7 @@ func (s *Service) UpdateTask(
 	if err := task.LoadJob(ctx); err != nil {
 		return nil, status.Errorf(codes.Internal, "load job: %v", err)
 	}
-	if err := task.Job.LoadRun(ctx); err != nil {
+	if err := task.Job.LoadAttributes(ctx); err != nil {
 		return nil, status.Errorf(codes.Internal, "load run: %v", err)
 	}
 
@@ -219,6 +220,10 @@ func (s *Service) UpdateTask(
 		actions_service.CreateCommitStatus(ctx, task.Job)
 	}
 
+	if task.Status.IsDone() {
+		notify_service.WorkflowJobStatusUpdate(ctx, task.Job.Run.Repo, task.Job.Run.TriggerUser, task.Job, task)
+	}
+
 	if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED {
 		if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil {
 			log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err)
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 9c49819970..ce0c1b5097 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -207,6 +207,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.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 4c39cb284f..41f0d2d0ec 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -33,6 +33,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	actions_service "code.gitea.io/gitea/services/actions"
 	context_module "code.gitea.io/gitea/services/context"
+	notify_service "code.gitea.io/gitea/services/notify"
 
 	"github.com/nektos/act/pkg/model"
 	"xorm.io/builder"
@@ -458,6 +459,9 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
 	}
 
 	actions_service.CreateCommitStatus(ctx, job)
+	_ = job.LoadAttributes(ctx)
+	notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+
 	return nil
 }
 
@@ -518,6 +522,8 @@ func Cancel(ctx *context_module.Context) {
 		return
 	}
 
+	var updatedjobs []*actions_model.ActionRunJob
+
 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 		for _, job := range jobs {
 			status := job.Status
@@ -534,6 +540,9 @@ func Cancel(ctx *context_module.Context) {
 				if n == 0 {
 					return fmt.Errorf("job has changed, try again")
 				}
+				if n > 0 {
+					updatedjobs = append(updatedjobs, job)
+				}
 				continue
 			}
 			if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
@@ -548,6 +557,11 @@ func Cancel(ctx *context_module.Context) {
 
 	actions_service.CreateCommitStatus(ctx, jobs...)
 
+	for _, job := range updatedjobs {
+		_ = job.LoadAttributes(ctx)
+		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+	}
+
 	ctx.JSON(http.StatusOK, struct{}{})
 }
 
@@ -561,6 +575,8 @@ func Approve(ctx *context_module.Context) {
 	run := current.Run
 	doer := ctx.Doer
 
+	var updatedjobs []*actions_model.ActionRunJob
+
 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 		run.NeedApproval = false
 		run.ApprovedBy = doer.ID
@@ -570,10 +586,13 @@ func Approve(ctx *context_module.Context) {
 		for _, job := range jobs {
 			if len(job.Needs) == 0 && job.Status.IsBlocked() {
 				job.Status = actions_model.StatusWaiting
-				_, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
+				n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
 				if err != nil {
 					return err
 				}
+				if n > 0 {
+					updatedjobs = append(updatedjobs, job)
+				}
 			}
 		}
 		return nil
@@ -584,6 +603,11 @@ func Approve(ctx *context_module.Context) {
 
 	actions_service.CreateCommitStatus(ctx, jobs...)
 
+	for _, job := range updatedjobs {
+		_ = job.LoadAttributes(ctx)
+		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+	}
+
 	ctx.JSON(http.StatusOK, struct{}{})
 }
 
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index 6875584d0b..d3151a86a2 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.HookEventWorkflowJob:              form.WorkflowJob,
 		},
 		BranchFilter: form.BranchFilter,
 	}
diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go
index 9d613b68a5..2aeb0e8c96 100644
--- a/services/actions/clear_tasks.go
+++ b/services/actions/clear_tasks.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	notify_service "code.gitea.io/gitea/services/notify"
 )
 
 // StopZombieTasks stops the task which have running status, but haven't been updated for a long time
@@ -37,6 +38,10 @@ func StopEndlessTasks(ctx context.Context) error {
 func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.ActionRunJob) {
 	if len(jobs) > 0 {
 		CreateCommitStatus(ctx, jobs...)
+		for _, job := range jobs {
+			_ = job.LoadAttributes(ctx)
+			notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+		}
 	}
 }
 
@@ -107,14 +112,20 @@ func CancelAbandonedJobs(ctx context.Context) error {
 	for _, job := range jobs {
 		job.Status = actions_model.StatusCancelled
 		job.Stopped = now
+		updated := false
 		if err := db.WithTx(ctx, func(ctx context.Context) error {
-			_, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped")
+			n, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped")
+			updated = err == nil && n > 0
 			return err
 		}); err != nil {
 			log.Warn("cancel abandoned job %v: %v", job.ID, err)
 			// go on
 		}
 		CreateCommitStatus(ctx, job)
+		if updated {
+			_ = job.LoadAttributes(ctx)
+			notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+		}
 	}
 
 	return nil
diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index 1f859fcf70..c11bb5875f 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/queue"
+	notify_service "code.gitea.io/gitea/services/notify"
 
 	"github.com/nektos/act/pkg/jobparser"
 	"xorm.io/builder"
@@ -49,6 +50,7 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
 	if err != nil {
 		return err
 	}
+	var updatedjobs []*actions_model.ActionRunJob
 	if err := db.WithTx(ctx, func(ctx context.Context) error {
 		idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
 		for _, job := range jobs {
@@ -64,6 +66,7 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
 				} else if n != 1 {
 					return fmt.Errorf("no affected for updating blocked job %v", job.ID)
 				}
+				updatedjobs = append(updatedjobs, job)
 			}
 		}
 		return nil
@@ -71,6 +74,10 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
 		return err
 	}
 	CreateCommitStatus(ctx, jobs...)
+	for _, job := range updatedjobs {
+		_ = job.LoadAttributes(ctx)
+		notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+	}
 	return nil
 }
 
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 87ea1a37f5..d179134798 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -27,6 +27,7 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 	"code.gitea.io/gitea/services/convert"
+	notify_service "code.gitea.io/gitea/services/notify"
 
 	"github.com/nektos/act/pkg/jobparser"
 	"github.com/nektos/act/pkg/model"
@@ -363,6 +364,9 @@ func handleWorkflows(
 			continue
 		}
 		CreateCommitStatus(ctx, alljobs...)
+		for _, job := range alljobs {
+			notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil)
+		}
 	}
 	return nil
 }
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index ad1158313b..a30b166063 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/timeutil"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	notify_service "code.gitea.io/gitea/services/notify"
 
 	"github.com/nektos/act/pkg/jobparser"
 )
@@ -148,6 +149,17 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
 	if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
 		return err
 	}
+	allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
+	if err != nil {
+		log.Error("FindRunJobs: %v", err)
+	}
+	err = run.LoadAttributes(ctx)
+	if err != nil {
+		log.Error("LoadAttributes: %v", err)
+	}
+	for _, job := range allJobs {
+		notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil)
+	}
 
 	// Return nil if no errors occurred
 	return nil
diff --git a/services/actions/task.go b/services/actions/task.go
index bc54ade347..1feeb67a80 100644
--- a/services/actions/task.go
+++ b/services/actions/task.go
@@ -10,6 +10,7 @@ import (
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	secret_model "code.gitea.io/gitea/models/secret"
+	notify_service "code.gitea.io/gitea/services/notify"
 
 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
 	"google.golang.org/protobuf/types/known/structpb"
@@ -17,8 +18,9 @@ import (
 
 func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) {
 	var (
-		task *runnerv1.Task
-		job  *actions_model.ActionRunJob
+		task       *runnerv1.Task
+		job        *actions_model.ActionRunJob
+		actionTask *actions_model.ActionTask
 	)
 
 	if err := db.WithTx(ctx, func(ctx context.Context) error {
@@ -34,6 +36,7 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
 			return fmt.Errorf("task LoadAttributes: %w", err)
 		}
 		job = t.Job
+		actionTask = t
 
 		secrets, err := secret_model.GetSecretsOfTask(ctx, t)
 		if err != nil {
@@ -74,6 +77,7 @@ func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
 	}
 
 	CreateCommitStatus(ctx, job)
+	notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, actionTask)
 
 	return task, true, nil
 }
diff --git a/services/actions/workflow.go b/services/actions/workflow.go
index 5225f4dcad..dc8a1dd349 100644
--- a/services/actions/workflow.go
+++ b/services/actions/workflow.go
@@ -25,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/convert"
+	notify_service "code.gitea.io/gitea/services/notify"
 
 	"github.com/nektos/act/pkg/jobparser"
 	"github.com/nektos/act/pkg/model"
@@ -276,6 +277,9 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re
 		log.Error("FindRunJobs: %v", err)
 	}
 	CreateCommitStatus(ctx, allJobs...)
+	for _, job := range allJobs {
+		notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil)
+	}
 
 	return nil
 }
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index f07186117e..1366d30b1f 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -237,6 +237,7 @@ type WebhookForm struct {
 	Release                  bool
 	Package                  bool
 	Status                   bool
+	WorkflowJob              bool
 	Active                   bool
 	BranchFilter             string `binding:"GlobPattern"`
 	AuthorizationHeader      string
diff --git a/services/notify/notifier.go b/services/notify/notifier.go
index 29bbb5702b..40428454be 100644
--- a/services/notify/notifier.go
+++ b/services/notify/notifier.go
@@ -6,6 +6,7 @@ package notify
 import (
 	"context"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	packages_model "code.gitea.io/gitea/models/packages"
@@ -77,4 +78,6 @@ type Notifier interface {
 	ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository)
 
 	CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus)
+
+	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 c97d0fcbaf..9f8be4b577 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -6,6 +6,7 @@ package notify
 import (
 	"context"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	packages_model "code.gitea.io/gitea/models/packages"
@@ -374,3 +375,9 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit
 		notifier.CreateCommitStatus(ctx, repo, commit, sender, status)
 	}
 }
+
+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 7354efd701..9c794a2342 100644
--- a/services/notify/null.go
+++ b/services/notify/null.go
@@ -6,6 +6,7 @@ package notify
 import (
 	"context"
 
+	actions_model "code.gitea.io/gitea/models/actions"
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	packages_model "code.gitea.io/gitea/models/packages"
@@ -212,3 +213,6 @@ 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) 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 3ea8f50764..5afca8d65a 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) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) {
+	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
+
+	return createDingtalkPayload(text, text, "Workflow Job", p.WorkflowJob.HTMLURL), nil
+}
+
 func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
 	return DingtalkPayload{
 		MsgType: "actionCard",
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 43e5e533bf..0a7eb0b166 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -271,6 +271,12 @@ func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, er
 	return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
 }
 
+func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) {
+	text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
+
+	return d.createPayload(p.Sender, text, "", p.WorkflowJob.HTMLURL, color), nil
+}
+
 func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	meta := &DiscordMeta{}
 	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 639118d2a5..274aaf90b3 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) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) {
+	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
+
+	return newFeishuTextPayload(text), nil
+}
+
 func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
 	return newJSONRequest(pc, w, t, true)
diff --git a/services/webhook/general.go b/services/webhook/general.go
index c3e2ded204..ea75038faf 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -325,6 +325,37 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte
 	return text, color
 }
 
+func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+	description := p.WorkflowJob.Conclusion
+	if description == "" {
+		description = p.WorkflowJob.Status
+	}
+	refLink := linkFormatter(p.WorkflowJob.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowJob.Name, p.WorkflowJob.RunID)+"["+base.ShortSha(p.WorkflowJob.HeadSha)+"]:"+description)
+
+	text = fmt.Sprintf("Workflow Job %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
+}
+
 // ToHook convert models.Webhook to api.Hook
 // This function is not part of the convert package to prevent an import cycle
 func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 034c0caf96..5bc7ba097e 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) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) {
+	text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
+
+	return m.newPayload(text)
+}
+
 var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
 
 func getMessageBody(htmlText string) string {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 485f695be2..f70e235f20 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -317,6 +317,20 @@ func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, er
 	), nil
 }
 
+func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) {
+	title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
+
+	return createMSTeamsPayload(
+		p.Repo,
+		p.Sender,
+		title,
+		"",
+		p.WorkflowJob.HTMLURL,
+		color,
+		&MSTeamsFact{"WorkflowJob:", p.WorkflowJob.Name},
+	), nil
+}
+
 func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
 	facts := make([]MSTeamsFact, 0, 2)
 	if r != nil {
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 76d6fd3472..7d779cd527 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -5,7 +5,10 @@ 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"
@@ -941,3 +944,114 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo
 		log.Error("PrepareWebhooks: %v", err)
 	}
 }
+
+func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) {
+	source := EventSource{
+		Repository: repo,
+		Owner:      repo.Owner,
+	}
+
+	var org *api.Organization
+	if repo.Owner.IsOrganization() {
+		org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner))
+	}
+
+	err := job.LoadAttributes(ctx)
+	if err != nil {
+		log.Error("Error loading job attributes: %v", err)
+		return
+	}
+
+	jobIndex := 0
+	jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
+	if err != nil {
+		log.Error("Error loading getting run jobs: %v", err)
+		return
+	}
+	for i, j := range jobs {
+		if j.ID == job.ID {
+			jobIndex = i
+			break
+		}
+	}
+
+	status, conclusion := toActionStatus(job.Status)
+	var runnerID int64
+	var runnerName string
+	var steps []*api.ActionWorkflowStep
+
+	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(),
+			})
+		}
+	}
+
+	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(),
+		},
+		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 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 6864fc822a..8829d95da6 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) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
+}
+
 func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	meta := &PackagistMeta{}
 	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index d98c20f479..adb7243fb1 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)
+	WorkflowJob(*api.WorkflowJobPayload) (T, error)
 }
 
 func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) {
@@ -80,6 +81,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.HookEventWorkflowJob:
+		return convertUnmarshalledJSON(rc.WorkflowJob, data)
 	}
 	return t, fmt.Errorf("newPayload unsupported event: %s", event)
 }
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 80ed747fd1..589ef3fe9b 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) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) {
+	text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true)
+
+	return s.createPayload(text, nil), nil
+}
+
 // Push implements payloadConvertor Push method
 func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
 	// n new commits
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 485e2d990b..ca74eabe1c 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) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) {
+	text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true)
+
+	return createTelegramPayloadHTML(text), nil
+}
+
 func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
 	// https://core.telegram.org/bots/api#formatting-options
 	return TelegramPayload{
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 1c834b4020..2b19822caf 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) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) {
+	text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
+
+	return newWechatworkMarkdownPayload(text), nil
+}
+
 func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
 	return newJSONRequest(pc, w, t, true)
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index 3b28a4c6c0..16ad263e42 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -259,6 +259,20 @@
 				</div>
 			</div>
 		</div>
+		<!-- Workflow Events -->
+		<div class="fourteen wide column">
+			<label>{{ctx.Locale.Tr "repo.settings.event_header_workflow"}}</label>
+		</div>
+		<!-- Workflow Job Event -->
+		<div class="seven wide column">
+			<div class="field">
+				<div class="ui checkbox">
+					<input name="workflow_job" type="checkbox" {{if .Webhook.HookEvents.Get "workflow_job"}}checked{{end}}>
+					<label>{{ctx.Locale.Tr "repo.settings.event_workflow_job"}}</label>
+					<span class="help">{{ctx.Locale.Tr "repo.settings.event_workflow_job_desc"}}</span>
+				</div>
+			</div>
+		</div>
 	</div>
 </div>
 
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
index 596ccce266..effeff111d 100644
--- a/tests/integration/repo_webhook_test.go
+++ b/tests/integration/repo_webhook_test.go
@@ -11,10 +11,12 @@ import (
 	"net/url"
 	"strings"
 	"testing"
+	"time"
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/models/webhook"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/json"
@@ -22,6 +24,7 @@ import (
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 	"code.gitea.io/gitea/tests"
 
+	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
 	"github.com/PuerkitoBio/goquery"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -605,3 +608,146 @@ func Test_WebhookStatus_NoWrongTrigger(t *testing.T) {
 		assert.EqualValues(t, "push", trigger)
 	})
 }
+
+func Test_WebhookWorkflowJob(t *testing.T) {
+	var payloads []api.WorkflowJobPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		assert.Contains(t, r.Header["X-Github-Event-Type"], "workflow_job", "X-GitHub-Event-Type should contain workflow_job")
+		assert.Contains(t, r.Header["X-Gitea-Event-Type"], "workflow_job", "X-Gitea-Event-Type should contain workflow_job")
+		assert.Contains(t, r.Header["X-Gogs-Event-Type"], "workflow_job", "X-Gogs-Event-Type should contain workflow_job")
+		content, _ := io.ReadAll(r.Body)
+		var payload api.WorkflowJobPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "workflow_job"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+		session := loginUser(t, "user2")
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "workflow_job")
+
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
+
+		gitRepo1, err := gitrepo.OpenRepository(t.Context(), repo1)
+		assert.NoError(t, err)
+
+		runner := newMockRunner()
+		runner.registerAsRepoRunner(t, "user2", "repo1", "mock-runner", []string{"ubuntu-latest"})
+
+		// 2. trigger the webhooks
+
+		// add workflow file to the repo
+		// init the workflow
+		wfTreePath := ".gitea/workflows/push.yml"
+		wfFileContent := `name: Push
+on: push
+jobs:
+  wf1-job:
+    runs-on: ubuntu-latest
+    steps:
+      - run: echo 'test the webhook'
+  wf2-job:
+    runs-on: ubuntu-latest
+    needs: wf1-job
+    steps:
+      - run: echo 'cmd 1'
+      - run: echo 'cmd 2'
+`
+		opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, fmt.Sprintf("create %s", wfTreePath), wfFileContent)
+		createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts)
+
+		commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch)
+		assert.NoError(t, err)
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "workflow_job", triggeredEvent)
+		assert.Len(t, payloads, 2)
+		assert.EqualValues(t, "queued", payloads[0].Action)
+		assert.EqualValues(t, "queued", payloads[0].WorkflowJob.Status)
+		assert.EqualValues(t, []string{"ubuntu-latest"}, payloads[0].WorkflowJob.Labels)
+		assert.EqualValues(t, commitID, payloads[0].WorkflowJob.HeadSha)
+		assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
+
+		assert.EqualValues(t, "waiting", payloads[1].Action)
+		assert.EqualValues(t, "waiting", payloads[1].WorkflowJob.Status)
+		assert.EqualValues(t, commitID, payloads[1].WorkflowJob.HeadSha)
+		assert.EqualValues(t, "repo1", payloads[1].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[1].Repo.FullName)
+
+		// 4. Execute a single Job
+		task := runner.fetchTask(t)
+		outcome := &mockTaskOutcome{
+			result:   runnerv1.Result_RESULT_SUCCESS,
+			execTime: time.Millisecond,
+		}
+		runner.execTask(t, task, outcome)
+
+		// 5. validate the webhook is triggered
+		assert.EqualValues(t, "workflow_job", triggeredEvent)
+		assert.Len(t, payloads, 5)
+		assert.EqualValues(t, "in_progress", payloads[2].Action)
+		assert.EqualValues(t, "in_progress", payloads[2].WorkflowJob.Status)
+		assert.EqualValues(t, "mock-runner", payloads[2].WorkflowJob.RunnerName)
+		assert.EqualValues(t, commitID, payloads[2].WorkflowJob.HeadSha)
+		assert.EqualValues(t, "repo1", payloads[2].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[2].Repo.FullName)
+
+		assert.EqualValues(t, "completed", payloads[3].Action)
+		assert.EqualValues(t, "completed", payloads[3].WorkflowJob.Status)
+		assert.EqualValues(t, "mock-runner", payloads[3].WorkflowJob.RunnerName)
+		assert.EqualValues(t, "success", payloads[3].WorkflowJob.Conclusion)
+		assert.EqualValues(t, commitID, payloads[3].WorkflowJob.HeadSha)
+		assert.EqualValues(t, "repo1", payloads[3].Repo.Name)
+		assert.EqualValues(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.HTMLURL, fmt.Sprintf("/jobs/%d", 0))
+		assert.Len(t, payloads[3].WorkflowJob.Steps, 1)
+
+		assert.EqualValues(t, "queued", payloads[4].Action)
+		assert.EqualValues(t, "queued", payloads[4].WorkflowJob.Status)
+		assert.EqualValues(t, []string{"ubuntu-latest"}, payloads[4].WorkflowJob.Labels)
+		assert.EqualValues(t, commitID, payloads[4].WorkflowJob.HeadSha)
+		assert.EqualValues(t, "repo1", payloads[4].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[4].Repo.FullName)
+
+		// 6. Execute a single Job
+		task = runner.fetchTask(t)
+		outcome = &mockTaskOutcome{
+			result:   runnerv1.Result_RESULT_FAILURE,
+			execTime: time.Millisecond,
+		}
+		runner.execTask(t, task, outcome)
+
+		// 7. validate the webhook is triggered
+		assert.EqualValues(t, "workflow_job", triggeredEvent)
+		assert.Len(t, payloads, 7)
+		assert.EqualValues(t, "in_progress", payloads[5].Action)
+		assert.EqualValues(t, "in_progress", payloads[5].WorkflowJob.Status)
+		assert.EqualValues(t, "mock-runner", payloads[5].WorkflowJob.RunnerName)
+
+		assert.EqualValues(t, commitID, payloads[5].WorkflowJob.HeadSha)
+		assert.EqualValues(t, "repo1", payloads[5].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[5].Repo.FullName)
+
+		assert.EqualValues(t, "completed", payloads[6].Action)
+		assert.EqualValues(t, "completed", payloads[6].WorkflowJob.Status)
+		assert.EqualValues(t, "failure", payloads[6].WorkflowJob.Conclusion)
+		assert.EqualValues(t, "mock-runner", payloads[6].WorkflowJob.RunnerName)
+		assert.EqualValues(t, commitID, payloads[6].WorkflowJob.HeadSha)
+		assert.EqualValues(t, "repo1", payloads[6].Repo.Name)
+		assert.EqualValues(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.HTMLURL, fmt.Sprintf("/jobs/%d", 1))
+		assert.Len(t, payloads[6].WorkflowJob.Steps, 2)
+	})
+}