diff --git a/cli/cli.go b/cli/cli.go
index 24f59394..40ae2afd 100644
--- a/cli/cli.go
+++ b/cli/cli.go
@@ -12,7 +12,7 @@ var Shortcuts = map[string]string{
 	"pull":           "builds:create",
 	"rollback":       "releases:rollback",
 	"run":            "apps:run",
-	"scale":          "ps:scale",
+	"scale":          "pts:scale",
 	"sharing":        "perms:list",
 	"sharing:list":   "perms:list",
 	"sharing:add":    "perms:create",
diff --git a/cmd/cmd.go b/cmd/cmd.go
index e525c378..9770a49b 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -37,10 +37,10 @@ type Commander interface {
 	CertAttach(string, string) error
 	CertDetach(string, string) error
 	ConfigList(string, string) error
-	ConfigSet(string, string, []string) error
-	ConfigUnset(string, string, []string) error
+	ConfigSet(string, string, []string, string) error
+	ConfigUnset(string, string, []string, string) error
 	ConfigPull(string, string, string, bool, bool) error
-	ConfigPush(string, string, string) error
+	ConfigPush(string, string, string, string) error
 	DomainsList(string, int) error
 	DomainsAdd(string, string, string) error
 	DomainsRemove(string, string) error
@@ -82,9 +82,12 @@ type Commander interface {
 	PsList(string, int) error
 	PsLogs(string, string, int, bool, string) error
 	PsExec(string, string, bool, bool, []string) error
-	PsScale(string, []string) error
-	PsRestart(string, []string, string) error
 	PsDescribe(string, string) error
+	PsDelete(string, []string) error
+	PtsList(string, int) error
+	PtsDescribe(string, string) error
+	PtsScale(string, []string) error
+	PtsRestart(string, []string, string) error
 	RegistryList(string) error
 	RegistrySet(string, []string) error
 	RegistryUnset(string, []string) error
diff --git a/cmd/config.go b/cmd/config.go
index 25858119..3a93e3de 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -1,11 +1,13 @@
 package cmd
 
 import (
+	"bufio"
 	"bytes"
 	"encoding/base64"
 	"fmt"
 	"os"
 	"regexp"
+	"runtime"
 	"strings"
 
 	"github.com/drycc/controller-sdk-go/api"
@@ -37,18 +39,36 @@ func (d *DryccCmd) ConfigList(appID string, procType string) error {
 			fmt.Sprintf("%v", configValues[key]),
 		})
 	}
+
+	// common config
+	if procType != "" {
+		table.Append([]string{"--- Common Config:"})
+		configValues = config.Values
+		keys = *sortKeys(configValues)
+		for _, key := range keys {
+			table.Append([]string{
+				key,
+				fmt.Sprintf("%v", configValues[key]),
+			})
+		}
+	}
 	table.Render()
 	return nil
 }
 
 // ConfigSet sets an app's config variables.
-func (d *DryccCmd) ConfigSet(appID string, procType string, configVars []string) error {
+func (d *DryccCmd) ConfigSet(appID string, procType string, configVars []string, confirm string) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
 		return err
 	}
 
+	err = configConfirmAction(procType, confirm)
+	if err != nil {
+		return err
+	}
+
 	configMap, err := parseConfig(configVars)
 	if err != nil {
 		return err
@@ -97,13 +117,18 @@ to set up healthchecks. This functionality has been deprecated. In the future, p
 }
 
 // ConfigUnset removes a config variable from an app.
-func (d *DryccCmd) ConfigUnset(appID string, procType string, configVars []string) error {
+func (d *DryccCmd) ConfigUnset(appID string, procType string, configVars []string, confirm string) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
 		return err
 	}
 
+	err = configConfirmAction(procType, confirm)
+	if err != nil {
+		return err
+	}
+
 	d.Print("Removing config... ")
 
 	quit := progress(d.WOut)
@@ -207,7 +232,7 @@ func (d *DryccCmd) ConfigPull(appID, procType, fileName string, interactive bool
 }
 
 // ConfigPush pushes an app's config from a file.
-func (d *DryccCmd) ConfigPush(appID, procType string, fileName string) error {
+func (d *DryccCmd) ConfigPush(appID, procType string, fileName string, confirm string) error {
 	stat, err := os.Stdin.Stat()
 
 	if err != nil {
@@ -217,10 +242,19 @@ func (d *DryccCmd) ConfigPush(appID, procType string, fileName string) error {
 	var contents []byte
 
 	if (stat.Mode() & os.ModeCharDevice) == 0 {
+		err = configConfirmActionStdin(procType, confirm)
+		if err != nil {
+			return err
+		}
 		buffer := new(bytes.Buffer)
 		buffer.ReadFrom(os.Stdin)
 		contents = buffer.Bytes()
 	} else {
+		err = configConfirmAction(procType, confirm)
+		if err != nil {
+			return err
+		}
+
 		contents, err = os.ReadFile(fileName)
 
 		if err != nil {
@@ -239,7 +273,7 @@ func (d *DryccCmd) ConfigPush(appID, procType string, fileName string) error {
 		}
 	}
 
-	return d.ConfigSet(appID, procType, config)
+	return d.ConfigSet(appID, procType, config, "yes")
 }
 
 func parseConfig(configVars []string) (api.ConfigValues, error) {
@@ -304,3 +338,52 @@ func formatConfig(configVars map[string]interface{}) string {
 
 	return formattedConfig
 }
+
+func configConfirmAction(procType string, confirm string) error {
+	if procType == "" && (confirm == "" || confirm != "yes") {
+		fmt.Printf(` !    WARNING: Potentially Config Action
+ !    This command will deploy all processes of the application
+ !    To proceed, type "yes" !
+
+> `)
+
+		fmt.Scanln(&confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the config action")
+		}
+	}
+	return nil
+}
+
+func configConfirmActionStdin(procType string, confirm string) error {
+	var reader *bufio.Reader
+	if runtime.GOOS == "windows" {
+		reader = bufio.NewReader(os.Stdin)
+	} else {
+		file, err := os.Open("/dev/tty")
+		if err != nil {
+			return err
+		}
+		defer file.Close()
+		reader = bufio.NewReader(file)
+	}
+
+	if procType == "" && (confirm == "" || confirm != "yes") {
+		fmt.Printf(` !    WARNING: Potentially Config Action
+ !    This command will deploy all processes of the application
+ !    To proceed, type "yes" !
+
+> `)
+
+		confirm, err := reader.ReadString('\n')
+		if err != nil {
+			return err
+		}
+
+		confirm = strings.TrimSpace(confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the config action")
+		}
+	}
+	return nil
+}
diff --git a/cmd/config_test.go b/cmd/config_test.go
index 12457a32..760d3501 100644
--- a/cmd/config_test.go
+++ b/cmd/config_test.go
@@ -136,8 +136,13 @@ TRUE     false
 	b.Reset()
 	err = cmdr.ConfigList("foo", "web")
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `NAME    VALUE 
-PORT    9000     
+	assert.Equal(t, b.String(), `NAME                  VALUE   
+PORT                  9000       
+--- Common Config:    
+FLOAT                 12.34      
+NCC                   1701       
+TEST                  testing    
+TRUE                  false      
 `, "output")
 
 }
@@ -184,7 +189,7 @@ func TestConfigSet(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.ConfigSet("foo", "", []string{"TRUE=false", "SSH_KEY=-----BEGIN OPENSSH PRIVATE KEY-----"})
+	err = cmdr.ConfigSet("foo", "", []string{"TRUE=false", "SSH_KEY=-----BEGIN OPENSSH PRIVATE KEY-----"}, "yes")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), `Creating config... done
@@ -238,7 +243,7 @@ func TestConfigUnset(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.ConfigUnset("foo", "", []string{"FOO"})
+	err = cmdr.ConfigUnset("foo", "", []string{"FOO"}, "yes")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), `Removing config... done
@@ -298,15 +303,17 @@ func TestConfigUnsetTypedValues(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.ConfigUnset("foo", "web", []string{"FOO"})
+	err = cmdr.ConfigUnset("foo", "web", []string{"FOO"}, "")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), `Removing config... done
 
-NAME     VALUE   
-FLOAT    12.34      
-NCC      1701       
-TEST     testing    
-TRUE     false      
+NAME                  VALUE   
+FLOAT                 12.34      
+NCC                   1701       
+TEST                  testing    
+TRUE                  false      
+--- Common Config:    
+RELEASE_VERSION       v1         
 `, "output")
 }
diff --git a/cmd/ps.go b/cmd/ps.go
index dd45e02e..b77706de 100644
--- a/cmd/ps.go
+++ b/cmd/ps.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/containerd/console"
 	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/events"
 	"github.com/drycc/controller-sdk-go/ps"
 	"github.com/drycc/workflow-cli/pkg/logging"
 	"golang.org/x/net/websocket"
@@ -103,76 +104,6 @@ func (d *DryccCmd) PsExec(appID, podID string, tty, stdin bool, command []string
 	return nil
 }
 
-// PsScale scales an app's processes.
-func (d *DryccCmd) PsScale(appID string, targets []string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-
-	targetMap, err := parsePsTargets(targets)
-	if err != nil {
-		return err
-	}
-
-	d.Printf("Scaling processes... but first, %s!\n", drinkOfChoice())
-	startTime := time.Now()
-	quit := progress(d.WOut)
-
-	err = ps.Scale(s.Client, appID, targetMap)
-	quit <- true
-	<-quit
-	if d.checkAPICompatibility(s.Client, err) != nil {
-		return err
-	}
-
-	d.Printf("done in %ds\n\n", int(time.Since(startTime).Seconds()))
-
-	processes, _, err := ps.List(s.Client, appID, s.Limit)
-	if err != nil {
-		return err
-	}
-
-	printProcesses(d, appID, processes)
-	return nil
-}
-
-// PsRestart restarts an app's processes.
-func (d *DryccCmd) PsRestart(appID string, targets []string, confirm string) error {
-	s, appID, err := load(d.ConfigFile, appID)
-	if err != nil {
-		return err
-	}
-	if len(targets) == 0 && confirm == "" {
-		d.Printf(` !    WARNING: Potentially Restart Action
- !    This command will restart all processes of the application
- !    To proceed, type "yes" !
-
-> `)
-
-		fmt.Scanln(&confirm)
-		if confirm != "yes" {
-			return fmt.Errorf("cancel the restart action")
-		}
-	}
-	d.Printf("Restarting processes... but first, %s!\n", drinkOfChoice())
-	startTime := time.Now()
-	quit := progress(d.WOut)
-	ptypes := strings.Join(targets, ",")
-	targetMap := map[string]string{
-		"types": ptypes,
-	}
-	err = ps.Restart(s.Client, appID, targetMap)
-	quit <- true
-	<-quit
-	if err != nil {
-		return err
-	}
-
-	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
-	return nil
-}
-
 // PsDescribe describe an app's processes.
 func (d *DryccCmd) PsDescribe(appID, podID string) error {
 	s, appID, err := load(d.ConfigFile, appID)
@@ -217,6 +148,47 @@ func (d *DryccCmd) PsDescribe(appID, podID string) error {
 		table.Append([]string{})
 	}
 	table.Render()
+	// display events
+	events, _, err := events.ListPodEvents(s.Client, appID, podID, 1000)
+	if err != nil {
+		return err
+	}
+	if len(events) != 0 {
+		// table event
+		te := d.getDefaultFormatTable([]string{})
+		te.Append([]string{"Events:"})
+		te.Append([]string{"  REASON", "MESSAGE", "CREATED"})
+		for _, ev := range events {
+			te.Append([]string{
+				fmt.Sprintf("  %s", ev.Reason),
+				ev.Message,
+				ev.Created.Format("2006-01-02T15:04:05MST"),
+			})
+		}
+		te.Render()
+	}
+	return nil
+}
+
+// PsDelete delete an pod.
+func (d *DryccCmd) PsDelete(appID string, podIDs []string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+
+	if err != nil {
+		return err
+	}
+	pods := strings.Join(podIDs, ",")
+	d.Printf("Deleting %s from %s... ", pods, appID)
+
+	quit := progress(d.WOut)
+	err = ps.Delete(s.Client, appID, pods)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Println("done")
 	return nil
 }
 
diff --git a/cmd/ps_test.go b/cmd/ps_test.go
index d8097d17..2eafa25d 100644
--- a/cmd/ps_test.go
+++ b/cmd/ps_test.go
@@ -162,98 +162,6 @@ func TestParsePsTargets(t *testing.T) {
 	}
 }
 
-func TestPsScale(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-	err = cmdr.PsScale("foo", []string{"test"})
-	assert.Equal(t, err.Error(), "'test' does not match the pattern 'type=num', ex: web=2", "error")
-
-	server.Mux.HandleFunc("/v2/apps/foo/pods/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-			"count": 1,
-			"next": null,
-			"previous": null,
-			"results": [
-				{
-					"release": "v2",
-					"type": "web",
-					"name": "foo-web-4084101150-c871y",
-					"state": "up",
-					"ready": "1/1",
-					"restarts": 0,
-					"started": "2016-02-13T00:47:52"
-				}
-			]
-		}`)
-	})
-
-	server.Mux.HandleFunc("/v2/apps/foo/scale/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, map[string]int{"web": 1}, r)
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	b.Reset()
-	err = cmdr.PsScale("foo", []string{"web=1"})
-	assert.NoError(t, err)
-	assert.Equal(t, testutil.StripProgress(b.String()), `Scaling processes... but first, coffee!
-done in 0s
-
-NAME                        RELEASE    STATE    PTYPE    READY    RESTARTS    STARTED                
-foo-web-4084101150-c871y    v2         up       web      1/1      0           2016-02-13T00:47:52UTC    
-`, "output")
-}
-
-func TestPsRestart(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	server.Mux.HandleFunc("/v2/apps/foo/pods/restart/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, map[string]string{"types": ""}, r)
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	b.Reset()
-	err = cmdr.PsRestart("foo", []string{""}, "yes")
-	assert.NoError(t, err)
-
-	server.Mux.HandleFunc("/v2/apps/coolapp/pods/restart/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, map[string]string{"types": "web"}, r)
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	b.Reset()
-
-	err = cmdr.PsRestart("coolapp", []string{"web"}, "")
-	assert.NoError(t, err)
-
-	server.Mux.HandleFunc("/v2/apps/testapp/pods/restart/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.AssertBody(t, map[string]string{"types": "web,worker"}, r)
-		testutil.SetHeaders(w)
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	b.Reset()
-
-	err = cmdr.PsRestart("testapp", []string{"web", "worker"}, "")
-	assert.NoError(t, err)
-}
-
 func TestPsDescribe(t *testing.T) {
 	t.Parallel()
 	cf, server, err := testutil.NewTestServerAndClient()
@@ -298,7 +206,36 @@ func TestPsDescribe(t *testing.T) {
 			}]
 		}`)
 	})
-
+	server.Mux.HandleFunc("/v2/apps/foo/events/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"count": 1,
+			"next": null,
+			"previous": null,
+			"results": [{
+				"reason": "Scheduled",
+				"message": "Successfully assigned example-go/example-go-web-6b44dbd6c8-h89cg to node1",
+				"created": "2024-07-03T16:28:00"
+			}]
+		}`)
+	})
 	err = cmdr.PsDescribe("foo", "foo-web-111")
 	assert.NoError(t, err)
 }
+
+func TestPsDelete(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+	server.Mux.HandleFunc("/v2/apps/foo/pods/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+	err = cmdr.PsDelete("foo", []string{"foo-web-111"})
+	assert.NoError(t, err)
+}
diff --git a/cmd/pts.go b/cmd/pts.go
new file mode 100644
index 00000000..fccc348a
--- /dev/null
+++ b/cmd/pts.go
@@ -0,0 +1,219 @@
+package cmd
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/drycc/controller-sdk-go/api"
+	"github.com/drycc/controller-sdk-go/events"
+	"github.com/drycc/controller-sdk-go/pts"
+)
+
+// PtsList lists an app's processes.
+func (d *DryccCmd) PtsList(appID string, results int) error {
+	s, appID, err := load(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	if results == defaultLimit {
+		results = s.Limit
+	}
+
+	ptypes, _, err := pts.List(s.Client, appID, results)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	printProcessTypes(d, appID, ptypes)
+
+	return nil
+}
+
+// PtsDescribe describe an app's processes.
+func (d *DryccCmd) PtsDescribe(appID, ptype string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	ptypeStates, _, err := pts.Describe(s.Client, appID, ptype, 1000)
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+	events, _, err := events.ListPtypeEvents(s.Client, appID, ptype, 1000)
+	if err != nil {
+		return err
+	}
+	printProcessTypeDetail(d, ptypeStates, events)
+	return nil
+}
+
+// PtsScale scales an app's processes.
+func (d *DryccCmd) PtsScale(appID string, targets []string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+
+	targetMap, err := parsePtsTargets(targets)
+	if err != nil {
+		return err
+	}
+
+	d.Printf("Scaling process types... but first, %s!\n", drinkOfChoice())
+	startTime := time.Now()
+	quit := progress(d.WOut)
+
+	err = pts.Scale(s.Client, appID, targetMap)
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
+
+	return nil
+}
+
+// PtsRestart restarts an app's processes.
+func (d *DryccCmd) PtsRestart(appID string, targets []string, confirm string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	if len(targets) == 0 && (confirm == "" || confirm != "yes") {
+		d.Printf(` !    WARNING: Potentially Restart Action
+ !    This command will restart all processes of the application ptype
+ !    To proceed, type "yes" !
+
+> `)
+
+		fmt.Scanln(&confirm)
+		if confirm != "yes" {
+			return fmt.Errorf("cancel the restart action")
+		}
+	}
+	d.Printf("Restarting process types... but first, %s!\n", drinkOfChoice())
+	startTime := time.Now()
+	quit := progress(d.WOut)
+	ptypes := strings.Join(targets, ",")
+	targetMap := map[string]string{
+		"types": ptypes,
+	}
+	err = pts.Restart(s.Client, appID, targetMap)
+	quit <- true
+	<-quit
+	if err != nil {
+		return err
+	}
+
+	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
+	return nil
+}
+
+func printProcessTypes(d *DryccCmd, appID string, ptypes api.Ptypes) {
+	pts := pts.ByType(ptypes)
+
+	if len(pts) == 0 {
+		d.Println(fmt.Sprintf("No processes found in %s app.", appID))
+	} else {
+		table := d.getDefaultFormatTable([]string{"NAME", "RELEASE", "READY", "UP-TO-DATE", "AVAILABLE", "STARTED"})
+		for _, pt := range pts {
+			table.Append([]string{
+				pt.Name,
+				pt.Release,
+				pt.Ready,
+				fmt.Sprintf("%v", pt.UpToDate),
+				fmt.Sprintf("%v", pt.AvailableReplicas),
+				pt.Started.Format("2006-01-02T15:04:05MST"),
+			})
+		}
+		table.Render()
+	}
+}
+
+func printProcessTypeDetail(d *DryccCmd, ptypeStates api.PtypeStates, events api.AppEvents) {
+	// table process type
+	tpt := d.getDefaultFormatTable([]string{})
+	for _, containerState := range ptypeStates {
+		// tpt.Append([]string{"Container: " + containerState.Container})
+		// tpt.Append([]string{"Image: " + containerState.Image})
+		tpt.Append([]string{"Container:", containerState.Container})
+		tpt.Append([]string{"Image:", containerState.Image})
+		if len(containerState.Command) != 0 {
+			tpt.Append([]string{"Command:"})
+			for _, command := range containerState.Command {
+				tpt.Append([]string{"", fmt.Sprintf("- %v", command)})
+			}
+		}
+		if len(containerState.Args) != 0 {
+			tpt.Append([]string{"Args:"})
+			for _, arg := range containerState.Args {
+				tpt.Append([]string{"", fmt.Sprintf("- %v", arg)})
+			}
+		}
+		if containerState.Limits != nil {
+			tpt.Append([]string{"Limits:"})
+			for r, q := range containerState.Limits {
+				tpt.Append([]string{"", fmt.Sprintf("%s %s", r, q)})
+			}
+		}
+		if len(containerState.VolumeMounts) != 0 {
+			tpt.Append([]string{"Mounts:"})
+			for _, mount := range containerState.VolumeMounts {
+				tpt.Append([]string{"", fmt.Sprintf("%s from %s", mount.MountPath, mount.Name)})
+			}
+		}
+		lp := getHealthcheckString("", "", &containerState.LivenessProbe)
+		if lp != "" {
+			tpt.Append([]string{"Liveness:", strings.TrimSpace(lp)})
+		}
+		rp := getHealthcheckString("", "", &containerState.ReadinessProbe)
+		if rp != "" {
+			tpt.Append([]string{"Readiness:", strings.TrimSpace(rp)})
+		}
+
+	}
+	tpt.Render()
+
+	if len(events) != 0 {
+		// table event
+		te := d.getDefaultFormatTable([]string{})
+		te.Append([]string{"Events:"})
+		te.Append([]string{"  REASON", "MESSAGE", "CREATED"})
+		for _, ev := range events {
+			te.Append([]string{
+				fmt.Sprintf("  %s", ev.Reason),
+				ev.Message,
+				ev.Created.Format("2006-01-02T15:04:05MST"),
+			})
+		}
+		te.Render()
+	}
+}
+
+func parsePtsTargets(targets []string) (map[string]int, error) {
+	targetMap := make(map[string]int)
+	regex := regexp.MustCompile(`^([a-z0-9]+(?:-[a-z0-9]+)*)=([0-9]+)$`)
+	var err error
+
+	for _, target := range targets {
+		if regex.MatchString(target) {
+			captures := regex.FindStringSubmatch(target)
+			targetMap[captures[1]], err = strconv.Atoi(captures[2])
+
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			return nil, fmt.Errorf("'%s' does not match the pattern 'type=num', ex: web=2", target)
+		}
+	}
+
+	return targetMap, nil
+}
diff --git a/cmd/pts_test.go b/cmd/pts_test.go
new file mode 100644
index 00000000..c178e7b2
--- /dev/null
+++ b/cmd/pts_test.go
@@ -0,0 +1,249 @@
+package cmd
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/drycc/controller-sdk-go/api"
+	dtime "github.com/drycc/controller-sdk-go/pkg/time"
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPrintProcessTypes(t *testing.T) {
+	var b bytes.Buffer
+	d, err := time.Parse("2006-01-02T15:04:05MST", "2024-07-04T14:33:00CST")
+	if err != nil {
+		t.Fatal(err)
+	}
+	ptypes := api.Ptypes{
+		{
+			Name:              "web",
+			Release:           "v1",
+			Ready:             "1/1",
+			UpToDate:          1,
+			AvailableReplicas: 1,
+			Started:           dtime.Time{Time: &d},
+		},
+		{
+			Name:              "worker",
+			Release:           "v1",
+			Ready:             "1/1",
+			UpToDate:          1,
+			AvailableReplicas: 1,
+			Started:           dtime.Time{Time: &d},
+		},
+	}
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+
+	printProcessTypes(&DryccCmd{WOut: &b, ConfigFile: cf}, "appname", ptypes)
+
+	assert.Equal(t, b.String(), `NAME      RELEASE    READY    UP-TO-DATE    AVAILABLE    STARTED                
+web       v1         1/1      1             1            2024-07-04T14:33:00CST    
+worker    v1         1/1      1             1            2024-07-04T14:33:00CST    
+`, "output")
+}
+
+func TestPtsList(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/ptypes/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+			"count": 1,
+			"next": null,
+			"previous": null,
+			"results": [
+				{
+					"name": "web",
+					"release": "v1",
+					"ready": "1/1",
+					"up_to_date": 1,
+					"available_replicas": 1,
+					"started": "2016-02-13T00:47:52"
+				}
+			]
+		}`)
+	})
+
+	err = cmdr.PtsList("foo", -1)
+	assert.NoError(t, err)
+
+	assert.Equal(t, b.String(), `NAME    RELEASE    READY    UP-TO-DATE    AVAILABLE    STARTED                
+web     v1         1/1      1             1            2016-02-13T00:47:52UTC    
+`, "output")
+}
+
+type ptsTargetCases struct {
+	Targets       []string
+	ExpectedError bool
+	ExpectedMap   map[string]int
+	ExpectedMsg   string
+}
+
+func TestParsePtsTargets(t *testing.T) {
+	t.Parallel()
+
+	cases := []ptsTargetCases{
+		{[]string{"test"}, true, nil, "'test' does not match the pattern 'type=num', ex: web=2"},
+		{[]string{"test=a"}, true, nil, "'test=a' does not match the pattern 'type=num', ex: web=2"},
+		{[]string{"test="}, true, nil, "'test=' does not match the pattern 'type=num', ex: web=2"},
+		{[]string{"Test=2"}, true, nil, "'Test=2' does not match the pattern 'type=num', ex: web=2"},
+		{[]string{"test=2"}, false, map[string]int{"test": 2}, ""},
+		{[]string{"test-proc=2"}, false, map[string]int{"test-proc": 2}, ""},
+		{[]string{"test1=2"}, false, map[string]int{"test1": 2}, ""},
+	}
+
+	for _, check := range cases {
+		actual, err := parsePtsTargets(check.Targets)
+		if check.ExpectedError {
+			assert.Equal(t, err.Error(), check.ExpectedMsg, "error")
+		} else {
+			assert.NoError(t, err)
+			assert.Equal(t, actual, check.ExpectedMap, "error")
+		}
+	}
+}
+
+func TestPtsScale(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+	err = cmdr.PtsScale("foo", []string{"test"})
+	assert.Equal(t, err.Error(), "'test' does not match the pattern 'type=num', ex: web=2", "error")
+
+	server.Mux.HandleFunc("/v2/apps/foo/ptypes/scale/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]int{"web": 1}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+	err = cmdr.PtsScale("foo", []string{"web=1"})
+	assert.NoError(t, err)
+	assert.Equal(t, testutil.StripProgress(b.String()), `Scaling process types... but first, coffee!
+done in 0s
+`, "output")
+}
+
+func TestPtsRestart(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/foo/ptypes/restart/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]string{"types": ""}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+	err = cmdr.PtsRestart("foo", []string{""}, "yes")
+	assert.NoError(t, err)
+
+	server.Mux.HandleFunc("/v2/apps/coolapp/ptypes/restart/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]string{"types": "web"}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+
+	err = cmdr.PtsRestart("coolapp", []string{"web"}, "")
+	assert.NoError(t, err)
+
+	server.Mux.HandleFunc("/v2/apps/testapp/ptypes/restart/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]string{"types": "web,worker"}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+
+	err = cmdr.PtsRestart("testapp", []string{"web", "worker"}, "")
+	assert.NoError(t, err)
+}
+
+func TestPtsDescribe(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+	server.Mux.HandleFunc("/v2/apps/foo/ptypes/foo-web/describe/", func(w http.ResponseWriter, _ *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+            "count": 1,
+            "next": null,
+            "previous": null,
+            "results": [{
+                "container": "web",
+                "image": "registry.drycc.cc/base/base",
+                "command": ["bash", "-c"],
+                "args": ["sleep", "3600s"],
+                "readiness_probe": {
+                    "exec": {
+                        "command": ["ls", "-la"]
+                    },
+                    "failureThreshold": 3,
+                    "initialDelaySeconds": 50,
+                    "periodSeconds": 10,
+                    "successThreshold": 1,
+                    "timeoutSeconds": 50
+                },
+                "limits": {
+                    "cpu": "1",
+                    "memory": "2Gi"
+                },
+                "volume_mounts": [
+                    {
+                        "mountPath": "/data",
+                        "name": "myvolume"
+                    }
+                ]
+            }]
+        }`)
+	})
+	server.Mux.HandleFunc("/v2/apps/foo/events/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.SetHeaders(w)
+		fmt.Fprintf(w, `{
+            "count": 1,
+            "next": null,
+            "previous": null,
+            "results": [{
+                "reason": "ScalingReplicaSet",
+                "message": "Scaled up replica set example-go-web-6b44dbd6c8 to 2 from 1",
+                "created": "2024-07-03T16:28:00"
+            }]
+        }`)
+	})
+	err = cmdr.PtsDescribe("foo", "web")
+	assert.NoError(t, err)
+}
diff --git a/cmd/shortcuts_test.go b/cmd/shortcuts_test.go
index 0ca0fafc..5de5fba7 100644
--- a/cmd/shortcuts_test.go
+++ b/cmd/shortcuts_test.go
@@ -17,7 +17,7 @@ open -> apps:open
 pull -> builds:create
 rollback -> releases:rollback
 run -> apps:run
-scale -> ps:scale
+scale -> pts:scale
 sharing -> perms:list
 sharing:add -> perms:create
 sharing:list -> perms:list
diff --git a/cmd/utils.go b/cmd/utils.go
index ceb2d169..07d88c90 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -130,7 +130,7 @@ func (d *DryccCmd) formatTime(timeStr string) string {
 // wrapString wraps s into a paragraph of lines of length lim, with minimal raggedness.
 func (d *DryccCmd) wrapString(s string) string {
 	sa, _ := tablewriter.WrapString(s, defaultLines)
-	return strings.Join(sa, "\r\n")
+	return strings.Join(sa, "\n")
 }
 
 func sortKeys(data map[string]interface{}) *[]string {
diff --git a/drycc.go b/drycc.go
index f661b0cc..0f62087b 100644
--- a/drycc.go
+++ b/drycc.go
@@ -61,6 +61,7 @@ Subcommands, use 'drycc help [subcommand]' to learn more:
   limits        manage resource limits for your application
   perms         manage permissions for applications
   ps            manage processes inside an app container
+  pts           manage process types inside an app container
   registry      manage private registry information for your application
   releases      manage releases of an application
   routing       manage routability of an application
@@ -151,6 +152,8 @@ Use 'git push drycc main' to deploy to an application.
 		err = parser.Perms(argv, &cmdr)
 	case "ps":
 		err = parser.Ps(argv, &cmdr)
+	case "pts":
+		err = parser.Pts(argv, &cmdr)
 	case "registry":
 		err = parser.Registry(argv, &cmdr)
 	case "releases":
diff --git a/go.mod b/go.mod
index eb40273a..a028885e 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.22
 require (
 	github.com/containerd/console v1.0.4
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
-	github.com/drycc/controller-sdk-go v0.0.0-20240702013605-81d290b86004
+	github.com/drycc/controller-sdk-go v0.0.0-20240709083211-c3849cd23514
 	github.com/drycc/pkg v0.0.0-20240225112316-78fc9239f51f
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/stretchr/testify v1.9.0
diff --git a/go.sum b/go.sum
index 86d9ac58..d035f28f 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
-github.com/drycc/controller-sdk-go v0.0.0-20240702013605-81d290b86004 h1:WHOD++Rk9UHCOJvW2WhUaLpW/Kw2UBwcfEnUABwahCc=
-github.com/drycc/controller-sdk-go v0.0.0-20240702013605-81d290b86004/go.mod h1:a6WodXx5WyT9kOHHpOL62EIqJaYJeAr7NW1FCoKmG6Q=
+github.com/drycc/controller-sdk-go v0.0.0-20240709083211-c3849cd23514 h1:f30ztVlj3CTWqvDtXpo09Kz6DOe3rS8+MGxPwvJrli8=
+github.com/drycc/controller-sdk-go v0.0.0-20240709083211-c3849cd23514/go.mod h1:n6eQe1irJqjwLo/7t9+Dhdv6faSESQN+ATnZRBP3/Uc=
 github.com/drycc/pkg v0.0.0-20240225112316-78fc9239f51f h1:kgjvUQJeAszDoU1Vo4vTTE92KI8Av3JPb6Qn890niXg=
 github.com/drycc/pkg v0.0.0-20240225112316-78fc9239f51f/go.mod h1:n+QxGif6ha9CEoxVnlipxb9IdmerybcUSzTEDFkvjiA=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
diff --git a/parser/config.go b/parser/config.go
index 9dcfc7ab..5a06d14f 100644
--- a/parser/config.go
+++ b/parser/config.go
@@ -85,6 +85,8 @@ Options:
     the application that you wish to set.
   --type=<type>
     the procType for which the config needs to be set.
+  --confirm=yes
+    To proceed, type "yes".
 `
 
 	args, err := docopt.ParseArgs(usage, argv, "")
@@ -95,8 +97,8 @@ Options:
 
 	app := safeGetString(args, "--app")
 	procType := safeGetString(args, "--type")
-
-	return cmdr.ConfigSet(app, procType, args["<var>=<value>"].([]string))
+	confirm := safeGetString(args, "--confirm")
+	return cmdr.ConfigSet(app, procType, args["<var>=<value>"].([]string), confirm)
 }
 
 func configUnset(argv []string, cmdr cmd.Commander) error {
@@ -114,6 +116,8 @@ Options:
     the application that you wish to unset.
   --type=<type>
     the procType for which the config needs to be unset.
+  --confirm=yes
+    To proceed, type "yes".
 `
 
 	args, err := docopt.ParseArgs(usage, argv, "")
@@ -123,7 +127,8 @@ Options:
 	}
 	app := safeGetString(args, "--app")
 	procType := safeGetString(args, "--type")
-	return cmdr.ConfigUnset(app, procType, args["<key>"].([]string))
+	confirm := safeGetString(args, "--confirm")
+	return cmdr.ConfigUnset(app, procType, args["<key>"].([]string), confirm)
 }
 
 func configPull(argv []string, cmdr cmd.Commander) error {
@@ -181,6 +186,8 @@ Options:
     the procType for which the config needs to be push.
   --path=<path>
     a path leading to an environment file [default: .env]
+  --confirm=yes
+    To proceed, type "yes".
 `
 
 	args, err := docopt.ParseArgs(usage, argv, "")
@@ -192,5 +199,6 @@ Options:
 	app := safeGetString(args, "--app")
 	procType := safeGetString(args, "--type")
 	path := safeGetValue(args, "--path", ".env")
-	return cmdr.ConfigPush(app, procType, path)
+	confirm := safeGetString(args, "--confirm")
+	return cmdr.ConfigPush(app, procType, path, confirm)
 }
diff --git a/parser/config_test.go b/parser/config_test.go
index a9b9b465..624f65a1 100644
--- a/parser/config_test.go
+++ b/parser/config_test.go
@@ -16,11 +16,11 @@ func (d FakeDryccCmd) ConfigList(string, string) error {
 	return errors.New("config:list")
 }
 
-func (d FakeDryccCmd) ConfigSet(string, string, []string) error {
+func (d FakeDryccCmd) ConfigSet(string, string, []string, string) error {
 	return errors.New("config:set")
 }
 
-func (d FakeDryccCmd) ConfigUnset(string, string, []string) error {
+func (d FakeDryccCmd) ConfigUnset(string, string, []string, string) error {
 	return errors.New("config:unset")
 }
 
@@ -28,7 +28,7 @@ func (d FakeDryccCmd) ConfigPull(string, string, string, bool, bool) error {
 	return errors.New("config:pull")
 }
 
-func (d FakeDryccCmd) ConfigPush(string, string, string) error {
+func (d FakeDryccCmd) ConfigPush(string, string, string, string) error {
 	return errors.New("config:push")
 }
 
diff --git a/parser/ps.go b/parser/ps.go
index 0594f58e..481ea182 100644
--- a/parser/ps.go
+++ b/parser/ps.go
@@ -16,9 +16,8 @@ Valid commands for processes:
 ps:list        list application processes
 ps:logs        print the logs for a container
 ps:exec        execute a command in a container
-ps:restart     restart an application or process type
-ps:scale       scale processes (e.g. web=4 worker=2)
 ps:describe    print a detailed description of the selected process
+ps:delete      delete the selected processes
 
 Use 'drycc help [command]' to learn more.
 `
@@ -30,12 +29,10 @@ Use 'drycc help [command]' to learn more.
 		return psLogs(argv, cmdr)
 	case "ps:exec":
 		return psExec(argv, cmdr)
-	case "ps:restart":
-		return psRestart(argv, cmdr)
-	case "ps:scale":
-		return psScale(argv, cmdr)
 	case "ps:describe":
 		return psDescribe(argv, cmdr)
+	case "ps:delete":
+		return psDelete(argv, cmdr)
 	default:
 		if printHelp(argv, usage) {
 			return nil
@@ -84,7 +81,7 @@ Options:
   -a --app=<app>
     the uniquely identifiable name for the application.
   -n --lines=<lines>
-    the number of lines to display, value range 1-1000.
+    the number of lines to display, default to 300 lines, -1 showing all log lines
   -f --follow
     specify if the logs should be streamed.
   -c --container=<container>
@@ -99,7 +96,9 @@ Options:
 
 	app := safeGetString(args, "--app")
 	lines := safeGetInt(args, "--lines")
-	if lines <= 0 {
+	if lines < 0 {
+		lines = -1
+	} else if lines == 0 {
 		lines = 300
 	}
 	follow := safeGetBool(args, "--follow")
@@ -140,46 +139,14 @@ Options:
 	return cmdr.PsExec(app, pod, tty, stdin, command)
 }
 
-func psRestart(argv []string, cmdr cmd.Commander) error {
-	usage := `
-Restart an application or process type.
-
-Usage: drycc ps:restart [<type>...] [options]
-
-Arguments:
-  <type>
-    the process name as defined in your Procfile, such as 'web' or 'web worker'.
-
-Options:
-  -a --app=<app>
-    the uniquely identifiable name for the application.
-  --confirm=yes
-    To proceed, type "yes".
-`
-
-	args, err := docopt.ParseArgs(usage, argv, "")
-
-	if err != nil {
-		return err
-	}
-
-	apps := safeGetString(args, "--app")
-	confirm := safeGetString(args, "--confirm")
-	return cmdr.PsRestart(apps, args["<type>"].([]string), confirm)
-}
-
-func psScale(argv []string, cmdr cmd.Commander) error {
+func psDescribe(argv []string, cmdr cmd.Commander) error {
 	usage := `
-Scales an application's processes by type.
+Print a detailed description of the selected process.
 
-Usage: drycc ps:scale <type>=<num>... [options]
+Usage: drycc ps:describe <pod> [options]
 
 Arguments:
-  <type>
-    the process name as defined in your Procfile, such as 'web' or 'worker'.
-    Note that Dockerfile apps have a default 'cmd' process type.
-  <num>
-    the number of processes.
+  <pod> the pod name for the application.
 
 Options:
   -a --app=<app>
@@ -192,15 +159,16 @@ Options:
 		return err
 	}
 
-	apps := safeGetString(args, "--app")
-	return cmdr.PsScale(apps, args["<type>=<num>"].([]string))
+	app := safeGetString(args, "--app")
+	pod := safeGetString(args, "<pod>")
+	return cmdr.PsDescribe(app, pod)
 }
 
-func psDescribe(argv []string, cmdr cmd.Commander) error {
+func psDelete(argv []string, cmdr cmd.Commander) error {
 	usage := `
-Print a detailed description of the selected process.
+Delete the selected processes.
 
-Usage: drycc ps:describe <pod> [options]
+Usage: drycc ps:delete <pod>... [options]
 
 Arguments:
   <pod> the pod name for the application.
@@ -211,12 +179,9 @@ Options:
 `
 
 	args, err := docopt.ParseArgs(usage, argv, "")
-
 	if err != nil {
 		return err
 	}
-
 	app := safeGetString(args, "--app")
-	pod := safeGetString(args, "<pod>")
-	return cmdr.PsDescribe(app, pod)
+	return cmdr.PsDelete(app, args["<pod>"].([]string))
 }
diff --git a/parser/ps_test.go b/parser/ps_test.go
index e700b2f0..a5a0e700 100644
--- a/parser/ps_test.go
+++ b/parser/ps_test.go
@@ -24,17 +24,13 @@ func (d FakeDryccCmd) PsExec(string, string, bool, bool, []string) error {
 	return errors.New("ps:exec")
 }
 
-func (d FakeDryccCmd) PsScale(string, []string) error {
-	return errors.New("ps:scale")
-}
-
-func (d FakeDryccCmd) PsRestart(string, []string, string) error {
-	return errors.New("ps:restart")
-}
-
 func (d FakeDryccCmd) PsDescribe(string, string) error {
 	return errors.New("ps:describe")
 }
+
+func (d FakeDryccCmd) PsDelete(string, []string) error {
+	return errors.New("ps:delete")
+}
 func TestPs(t *testing.T) {
 	t.Parallel()
 
@@ -61,7 +57,7 @@ func TestPs(t *testing.T) {
 			expected: "",
 		},
 		{
-			args:     []string{"ps:restart", "web"},
+			args:     []string{"ps:exec", "myapp-web-111", "-it", "--", "bash"},
 			expected: "",
 		},
 		{
@@ -69,7 +65,11 @@ func TestPs(t *testing.T) {
 			expected: "",
 		},
 		{
-			args:     []string{"ps:list"},
+			args:     []string{"ps:describe", "myapp-web-111"},
+			expected: "",
+		},
+		{
+			args:     []string{"ps:delete", "myapp-web-111"},
 			expected: "",
 		},
 		{
diff --git a/parser/pts.go b/parser/pts.go
new file mode 100644
index 00000000..993fecc0
--- /dev/null
+++ b/parser/pts.go
@@ -0,0 +1,144 @@
+package parser
+
+import (
+	docopt "github.com/docopt/docopt-go"
+	"github.com/drycc/workflow-cli/cmd"
+)
+
+// Pts routes pts commands to their specific function.
+func Pts(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Valid commands for processes:
+
+pts:list        list application process types
+pts:describe    print a detailed description of the selected process type
+pts:restart     restart an application or process types
+pts:scale       scale process types of replicas (e.g. web=4 worker=2)
+
+Use 'drycc help [command]' to learn more.
+`
+
+	switch argv[0] {
+	case "pts:list":
+		return ptsList(argv, cmdr)
+	case "pts:describe":
+		return ptsDescribe(argv, cmdr)
+	case "pts:restart":
+		return ptsRestart(argv, cmdr)
+	case "pts:scale":
+		return ptsScale(argv, cmdr)
+	default:
+		if printHelp(argv, usage) {
+			return nil
+		}
+
+		if argv[0] == "pts" {
+			argv[0] = "pts:list"
+			return ptsList(argv, cmdr)
+		}
+
+		PrintUsage(cmdr)
+		return nil
+	}
+}
+
+func ptsList(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Lists process types servicing an application.
+
+Usage: drycc pts:list [options]
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+`
+
+	args, err := docopt.ParseArgs(usage, argv, "")
+	if err != nil {
+		return err
+	}
+
+	// The 1000 is fake for now until API understands limits
+	return cmdr.PtsList(safeGetString(args, "--app"), 1000)
+}
+
+func ptsDescribe(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Print a detailed description of the selected process type.
+
+Usage: drycc pts:describe <ptype> [options]
+
+Arguments:
+  <ptype> the process name as defined in your Procfile, such as 'web'.
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+`
+
+	args, err := docopt.ParseArgs(usage, argv, "")
+
+	if err != nil {
+		return err
+	}
+
+	app := safeGetString(args, "--app")
+	ptype := safeGetString(args, "<ptype>")
+	return cmdr.PtsDescribe(app, ptype)
+}
+
+func ptsRestart(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Restart an application or process types.
+
+Usage: drycc pts:restart [<ptype>...] [options]
+
+Arguments:
+  <ptype>
+    the process name as defined in your Procfile, such as 'web' or 'web worker'.
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+  --confirm=yes
+    To proceed, type "yes".
+`
+
+	args, err := docopt.ParseArgs(usage, argv, "")
+
+	if err != nil {
+		return err
+	}
+
+	apps := safeGetString(args, "--app")
+	confirm := safeGetString(args, "--confirm")
+	return cmdr.PtsRestart(apps, args["<ptype>"].([]string), confirm)
+}
+
+func ptsScale(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Scales an application's processes by type.
+
+Usage: drycc pts:scale <ptype>=<num>... [options]
+
+Arguments:
+  <ptype>
+    the process name as defined in your Procfile, such as 'web' or 'worker'.
+    Note that Dockerfile apps have a default 'cmd' process type.
+  <num>
+    the number of processes.
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+`
+
+	args, err := docopt.ParseArgs(usage, argv, "")
+
+	if err != nil {
+		return err
+	}
+
+	apps := safeGetString(args, "--app")
+	return cmdr.PtsScale(apps, args["<ptype>=<num>"].([]string))
+}
diff --git a/parser/pts_test.go b/parser/pts_test.go
new file mode 100644
index 00000000..301fb0c2
--- /dev/null
+++ b/parser/pts_test.go
@@ -0,0 +1,82 @@
+package parser
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"github.com/drycc/workflow-cli/pkg/testutil"
+	"github.com/stretchr/testify/assert"
+)
+
+// Create fake implementations of each method that return the argument
+// we expect to have called the function (as an error to satisfy the interface).
+
+func (d FakeDryccCmd) PtsList(string, int) error {
+	return errors.New("pts:list")
+}
+
+func (d FakeDryccCmd) PtsDescribe(string, string) error {
+	return errors.New("pts:describe")
+}
+
+func (d FakeDryccCmd) PtsScale(string, []string) error {
+	return errors.New("pts:scale")
+}
+
+func (d FakeDryccCmd) PtsRestart(string, []string, string) error {
+	return errors.New("pts:restart")
+}
+
+func TestPts(t *testing.T) {
+	t.Parallel()
+
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := FakeDryccCmd{WOut: &b, ConfigFile: cf}
+
+	// cases defines the arguments and expected return of the call.
+	// if expected is "", it defaults to args[0].
+	cases := []struct {
+		args     []string
+		expected string
+	}{
+		{
+			args:     []string{"pts:list"},
+			expected: "",
+		},
+		{
+			args:     []string{"pts:describe", "web"},
+			expected: "",
+		},
+		{
+			args:     []string{"pts:restart", "web"},
+			expected: "",
+		},
+		{
+			args:     []string{"pts:scale", "web", "5"},
+			expected: "",
+		},
+		{
+			args:     []string{"pts"},
+			expected: "pts:list",
+		},
+	}
+
+	// For each case, check that calling the route with the arguments
+	// returns the expected error, which is args[0] if not provided.
+	for _, c := range cases {
+		var expected string
+		if c.expected == "" {
+			expected = c.args[0]
+		} else {
+			expected = c.expected
+		}
+		err = Pts(c.args, cmdr)
+		assert.Error(t, errors.New(expected), err)
+	}
+}
