diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go
index 36faf48a4..d486837f9 100644
--- a/cmd/server/docs/docs.go
+++ b/cmd/server/docs/docs.go
@@ -4004,11 +4004,31 @@ const docTemplate = `{
                 "state": {
                     "$ref": "#/definitions/StatusValue"
                 },
+                "type": {
+                    "$ref": "#/definitions/StepType"
+                },
                 "uuid": {
                     "type": "string"
                 }
             }
         },
+        "StepType": {
+            "type": "string",
+            "enum": [
+                "clone",
+                "service",
+                "plugin",
+                "commands",
+                "cache"
+            ],
+            "x-enum-varnames": [
+                "StepTypeClone",
+                "StepTypeService",
+                "StepTypePlugin",
+                "StepTypeCommands",
+                "StepTypeCache"
+            ]
+        },
         "Task": {
             "type": "object",
             "properties": {
diff --git a/pipeline/backend/types/step.go b/pipeline/backend/types/step.go
index 7c14f1271..6143ebc90 100644
--- a/pipeline/backend/types/step.go
+++ b/pipeline/backend/types/step.go
@@ -4,6 +4,7 @@ package types
 type Step struct {
 	Name           string            `json:"name"`
 	UUID           string            `json:"uuid"`
+	Type           StepType          `json:"type,omitempty"`
 	Alias          string            `json:"alias,omitempty"`
 	Image          string            `json:"image,omitempty"`
 	Pull           bool              `json:"pull,omitempty"`
@@ -35,3 +36,14 @@ type Step struct {
 	Sysctls        map[string]string `json:"sysctls,omitempty"`
 	BackendOptions BackendOptions    `json:"backend_options,omitempty"`
 }
+
+// StepType identifies the type of step
+type StepType string
+
+const (
+	StepTypeClone    StepType = "clone"
+	StepTypeService  StepType = "service"
+	StepTypePlugin   StepType = "plugin"
+	StepTypeCommands StepType = "commands"
+	StepTypeCache    StepType = "cache"
+)
diff --git a/pipeline/frontend/yaml/compiler/compiler.go b/pipeline/frontend/yaml/compiler/compiler.go
index ae94ca071..7525fff28 100644
--- a/pipeline/frontend/yaml/compiler/compiler.go
+++ b/pipeline/frontend/yaml/compiler/compiler.go
@@ -15,7 +15,6 @@ const (
 	defaultCloneName = "clone"
 
 	nameServices = "services"
-	namePipeline = "pipeline"
 )
 
 // Registry represents registry credentials
@@ -150,7 +149,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
 			Environment: c.cloneEnv,
 		}
 		name := fmt.Sprintf("%s_clone", c.prefix)
-		step := c.createProcess(name, container, defaultCloneName)
+		step := c.createProcess(name, container, backend_types.StepTypeClone)
 
 		stage := new(backend_types.Stage)
 		stage.Name = name
@@ -171,7 +170,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
 			stage.Alias = container.Name
 
 			name := fmt.Sprintf("%s_clone_%d", c.prefix, i)
-			step := c.createProcess(name, container, defaultCloneName)
+			step := c.createProcess(name, container, backend_types.StepTypeClone)
 
 			// only inject netrc if it's a trusted repo or a trusted plugin
 			if !c.netrcOnlyTrusted || c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage()) {
@@ -202,7 +201,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
 			}
 
 			name := fmt.Sprintf("%s_%s_%d", c.prefix, nameServices, i)
-			step := c.createProcess(name, container, nameServices)
+			step := c.createProcess(name, container, backend_types.StepTypeService)
 			stage.Steps = append(stage.Steps, step)
 		}
 		config.Stages = append(config.Stages, stage)
@@ -233,7 +232,11 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
 		}
 
 		name := fmt.Sprintf("%s_step_%d", c.prefix, i)
-		step := c.createProcess(name, container, namePipeline)
+		stepType := backend_types.StepTypeCommands
+		if container.IsPlugin() {
+			stepType = backend_types.StepTypePlugin
+		}
+		step := c.createProcess(name, container, stepType)
 		stage.Steps = append(stage.Steps, step)
 	}
 
@@ -249,7 +252,7 @@ func (c *Compiler) setupCache(conf *yaml_types.Workflow, ir *backend_types.Confi
 
 	container := c.cacher.Restore(path.Join(c.metadata.Repo.Owner, c.metadata.Repo.Name), c.metadata.Curr.Commit.Branch, conf.Cache)
 	name := fmt.Sprintf("%s_restore_cache", c.prefix)
-	step := c.createProcess(name, container, "cache")
+	step := c.createProcess(name, container, backend_types.StepTypeCache)
 
 	stage := new(backend_types.Stage)
 	stage.Name = name
@@ -266,7 +269,7 @@ func (c *Compiler) setupCacheRebuild(conf *yaml_types.Workflow, ir *backend_type
 	container := c.cacher.Rebuild(path.Join(c.metadata.Repo.Owner, c.metadata.Repo.Name), c.metadata.Curr.Commit.Branch, conf.Cache)
 
 	name := fmt.Sprintf("%s_rebuild_cache", c.prefix)
-	step := c.createProcess(name, container, "cache")
+	step := c.createProcess(name, container, backend_types.StepTypeCache)
 
 	stage := new(backend_types.Stage)
 	stage.Name = name
diff --git a/pipeline/frontend/yaml/compiler/convert.go b/pipeline/frontend/yaml/compiler/convert.go
index 27c5c4e20..6a4754dfc 100644
--- a/pipeline/frontend/yaml/compiler/convert.go
+++ b/pipeline/frontend/yaml/compiler/convert.go
@@ -17,7 +17,7 @@ import (
 	"github.com/woodpecker-ci/woodpecker/pipeline/frontend/yaml/utils"
 )
 
-func (c *Compiler) createProcess(name string, container *yaml_types.Container, section string) *backend_types.Step {
+func (c *Compiler) createProcess(name string, container *yaml_types.Container, stepType backend_types.StepType) *backend_types.Step {
 	var (
 		uuid = uuid.New()
 
@@ -60,7 +60,7 @@ func (c *Compiler) createProcess(name string, container *yaml_types.Container, s
 	environment["CI_WORKSPACE"] = path.Join(c.base, c.path)
 	environment["CI_STEP_NAME"] = name
 
-	if section == "services" || container.Detached {
+	if stepType == backend_types.StepTypeService || container.Detached {
 		detached = true
 	}
 
@@ -152,6 +152,7 @@ func (c *Compiler) createProcess(name string, container *yaml_types.Container, s
 	return &backend_types.Step{
 		Name:           name,
 		UUID:           uuid.String(),
+		Type:           stepType,
 		Alias:          container.Name,
 		Image:          container.Image,
 		Pull:           container.Pull,
diff --git a/pipeline/rpc/proto/woodpecker.pb.go b/pipeline/rpc/proto/woodpecker.pb.go
index 230270750..0aa8163ff 100644
--- a/pipeline/rpc/proto/woodpecker.pb.go
+++ b/pipeline/rpc/proto/woodpecker.pb.go
@@ -16,7 +16,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.30.0
-// 	protoc        v3.21.12
+// 	protoc        v4.23.3
 // source: woodpecker.proto
 
 package proto
diff --git a/pipeline/rpc/proto/woodpecker_grpc.pb.go b/pipeline/rpc/proto/woodpecker_grpc.pb.go
index 44c2814fa..b0a161be3 100644
--- a/pipeline/rpc/proto/woodpecker_grpc.pb.go
+++ b/pipeline/rpc/proto/woodpecker_grpc.pb.go
@@ -16,7 +16,7 @@
 // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
 // versions:
 // - protoc-gen-go-grpc v1.3.0
-// - protoc             v3.21.12
+// - protoc             v4.23.3
 // source: woodpecker.proto
 
 package proto
diff --git a/pipeline/stepBuilder.go b/pipeline/stepBuilder.go
index 1f8d8204e..2def2fadd 100644
--- a/pipeline/stepBuilder.go
+++ b/pipeline/stepBuilder.go
@@ -316,6 +316,7 @@ func SetPipelineStepsOnPipeline(pipeline *model.Pipeline, pipelineItems []*Item)
 					PPID:       item.Workflow.PID,
 					State:      model.StatusPending,
 					Failure:    step.Failure,
+					Type:       model.StepType(step.Type),
 				}
 				if item.Workflow.State == model.StatusSkipped {
 					step.State = model.StatusSkipped
diff --git a/server/forge/mocks/forge.go b/server/forge/mocks/forge.go
index d39649740..4f7c2e3c1 100644
--- a/server/forge/mocks/forge.go
+++ b/server/forge/mocks/forge.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.29.0. DO NOT EDIT.
+// Code generated by mockery v2.31.1. DO NOT EDIT.
 
 package mocks
 
@@ -432,13 +432,12 @@ func (_m *Forge) URL() string {
 	return r0
 }
 
-type mockConstructorTestingTNewForge interface {
+// NewForge creates a new instance of Forge. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewForge(t interface {
 	mock.TestingT
 	Cleanup(func())
-}
-
-// NewForge creates a new instance of Forge. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
-func NewForge(t mockConstructorTestingTNewForge) *Forge {
+}) *Forge {
 	mock := &Forge{}
 	mock.Mock.Test(t)
 
diff --git a/server/model/step.go b/server/model/step.go
index bf0661c9e..edf40e611 100644
--- a/server/model/step.go
+++ b/server/model/step.go
@@ -47,6 +47,7 @@ type Step struct {
 	ExitCode   int         `json:"exit_code"            xorm:"step_exit_code"`
 	Started    int64       `json:"start_time,omitempty" xorm:"step_started"`
 	Stopped    int64       `json:"end_time,omitempty"   xorm:"step_stopped"`
+	Type       StepType    `json:"type,omitempty"       xorm:"step_type"`
 } //	@name Step
 
 type UpdateStepStore interface {
@@ -67,3 +68,14 @@ func (p *Step) Running() bool {
 func (p *Step) Failing() bool {
 	return p.Failure == FailureFail && (p.State == StatusError || p.State == StatusKilled || p.State == StatusFailure)
 }
+
+// StepType identifies the type of step
+type StepType string //	@name StepType
+
+const (
+	StepTypeClone    StepType = "clone"
+	StepTypeService  StepType = "service"
+	StepTypePlugin   StepType = "plugin"
+	StepTypeCommands StepType = "commands"
+	StepTypeCache    StepType = "cache"
+)
diff --git a/server/store/mocks/store.go b/server/store/mocks/store.go
index 7ead13c1c..47ce43184 100644
--- a/server/store/mocks/store.go
+++ b/server/store/mocks/store.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.29.0. DO NOT EDIT.
+// Code generated by mockery v2.31.1. DO NOT EDIT.
 
 package mocks
 
@@ -2021,13 +2021,12 @@ func (_m *Store) WorkflowsCreate(_a0 []*model.Workflow) error {
 	return r0
 }
 
-type mockConstructorTestingTNewStore interface {
+// NewStore creates a new instance of Store. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewStore(t interface {
 	mock.TestingT
 	Cleanup(func())
-}
-
-// NewStore creates a new instance of Store. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
-func NewStore(t mockConstructorTestingTNewStore) *Store {
+}) *Store {
 	mock := &Store{}
 	mock.Mock.Test(t)
 
diff --git a/web/src/lib/api/types/pipeline.ts b/web/src/lib/api/types/pipeline.ts
index 8fe461e3e..411e5a44e 100644
--- a/web/src/lib/api/types/pipeline.ts
+++ b/web/src/lib/api/types/pipeline.ts
@@ -126,6 +126,7 @@ export type PipelineStep = {
   start_time?: number;
   end_time?: number;
   error?: string;
+  type?: StepType;
 };
 
 export type PipelineLog = {
@@ -140,3 +141,11 @@ export type PipelineLog = {
 export type PipelineFeed = Pipeline & {
   repo_id: number;
 };
+
+export enum StepType {
+  Clone = 1,
+  Service,
+  Plugin,
+  Commands,
+  Cache,
+}
diff --git a/woodpecker-go/woodpecker/const.go b/woodpecker-go/woodpecker/const.go
index 70e5a5be5..8ba28522b 100644
--- a/woodpecker-go/woodpecker/const.go
+++ b/woodpecker-go/woodpecker/const.go
@@ -44,3 +44,14 @@ const (
 	LogEntryMetadata
 	LogEntryProgress
 )
+
+// StepType identifies the type of step
+type StepType string
+
+const (
+	StepTypeClone    StepType = "clone"
+	StepTypeService  StepType = "service"
+	StepTypePlugin   StepType = "plugin"
+	StepTypeCommands StepType = "commands"
+	StepTypeCache    StepType = "cache"
+)
diff --git a/woodpecker-go/woodpecker/types.go b/woodpecker-go/woodpecker/types.go
index ded0d4c01..48204390a 100644
--- a/woodpecker-go/woodpecker/types.go
+++ b/woodpecker-go/woodpecker/types.go
@@ -108,15 +108,16 @@ type (
 
 	// Step represents a process in the pipeline.
 	Step struct {
-		ID       int64  `json:"id"`
-		PID      int    `json:"pid"`
-		PPID     int    `json:"ppid"`
-		Name     string `json:"name"`
-		State    string `json:"state"`
-		Error    string `json:"error,omitempty"`
-		ExitCode int    `json:"exit_code"`
-		Started  int64  `json:"start_time,omitempty"`
-		Stopped  int64  `json:"end_time,omitempty"`
+		ID       int64    `json:"id"`
+		PID      int      `json:"pid"`
+		PPID     int      `json:"ppid"`
+		Name     string   `json:"name"`
+		State    string   `json:"state"`
+		Error    string   `json:"error,omitempty"`
+		ExitCode int      `json:"exit_code"`
+		Started  int64    `json:"start_time,omitempty"`
+		Stopped  int64    `json:"end_time,omitempty"`
+		Type     StepType `json:"type,omitempty"`
 	}
 
 	// Registry represents a docker registry with credentials.