diff --git a/temporalcli/commands.batch.go b/temporalcli/commands.batch.go
index eaf2a817..1f0a1e2c 100644
--- a/temporalcli/commands.batch.go
+++ b/temporalcli/commands.batch.go
@@ -70,6 +70,10 @@ func (c TemporalBatchListCommand) run(cctx *CommandContext, args []string) error
 	}
 	defer cl.Close()
 
+	// This is a listing command subject to json vs jsonl rules
+	cctx.Printer.StartList()
+	defer cctx.Printer.EndList()
+
 	pageFetcher := c.pageFetcher(cctx, cl)
 	var nextPageToken []byte
 	var jobsProcessed int
@@ -80,9 +84,6 @@ func (c TemporalBatchListCommand) run(cctx *CommandContext, args []string) error
 		}
 
 		if pageIndex == 0 && len(page.GetOperationInfo()) == 0 {
-			if cctx.JSONOutput {
-				_ = cctx.Printer.PrintStructured([]any{}, printer.StructuredOptions{})
-			}
 			return nil
 		}
 
diff --git a/temporalcli/commands.batch_test.go b/temporalcli/commands.batch_test.go
index eba6b72e..087217d4 100644
--- a/temporalcli/commands.batch_test.go
+++ b/temporalcli/commands.batch_test.go
@@ -97,7 +97,7 @@ func (s *SharedServerSuite) TestBatchJob_List() {
 			)
 			s.NoError(res.Err)
 			s.Empty(res.Stderr.String())
-			s.Equal("[]\n", res.Stdout.String())
+			s.Equal("[\n]\n", res.Stdout.String())
 		})
 	})
 
diff --git a/temporalcli/commands.gen.go b/temporalcli/commands.gen.go
index 0218cb0a..fad0c1a9 100644
--- a/temporalcli/commands.gen.go
+++ b/temporalcli/commands.gen.go
@@ -48,8 +48,8 @@ func NewTemporalCommand(cctx *CommandContext) *TemporalCommand {
 	s.Command.PersistentFlags().Var(&s.LogLevel, "log-level", "Log level. Accepted values: debug, info, warn, error, never.")
 	s.LogFormat = NewStringEnum([]string{"text", "json"}, "text")
 	s.Command.PersistentFlags().Var(&s.LogFormat, "log-format", "Log format. Accepted values: text, json.")
-	s.Output = NewStringEnum([]string{"text", "json"}, "text")
-	s.Command.PersistentFlags().VarP(&s.Output, "output", "o", "Data output format. Accepted values: text, json.")
+	s.Output = NewStringEnum([]string{"text", "json", "jsonl"}, "text")
+	s.Command.PersistentFlags().VarP(&s.Output, "output", "o", "Data output format. Accepted values: text, json, jsonl.")
 	s.TimeFormat = NewStringEnum([]string{"relative", "iso", "raw"}, "relative")
 	s.Command.PersistentFlags().Var(&s.TimeFormat, "time-format", "Time format. Accepted values: relative, iso, raw.")
 	s.Color = NewStringEnum([]string{"always", "never", "auto"}, "auto")
diff --git a/temporalcli/commands.go b/temporalcli/commands.go
index 720cf7e6..ee427b03 100644
--- a/temporalcli/commands.go
+++ b/temporalcli/commands.go
@@ -381,12 +381,17 @@ func (c *TemporalCommand) preRun(cctx *CommandContext) error {
 	}
 
 	// Configure printer if not already on context
-	cctx.JSONOutput = c.Output.Value == "json"
+	cctx.JSONOutput = c.Output.Value == "json" || c.Output.Value == "jsonl"
+	// Only indent JSON if not jsonl
+	var jsonIndent string
+	if c.Output.Value == "json" {
+		jsonIndent = "  "
+	}
 	if cctx.Printer == nil {
 		cctx.Printer = &printer.Printer{
 			Output:               cctx.Options.Stdout,
 			JSON:                 cctx.JSONOutput,
-			JSONIndent:           "  ",
+			JSONIndent:           jsonIndent,
 			JSONPayloadShorthand: !c.NoJsonShorthandPayloads,
 		}
 		switch c.TimeFormat.Value {
diff --git a/temporalcli/commands.operator_cluster.go b/temporalcli/commands.operator_cluster.go
index 9341b664..36539593 100644
--- a/temporalcli/commands.operator_cluster.go
+++ b/temporalcli/commands.operator_cluster.go
@@ -95,6 +95,10 @@ func (c *TemporalOperatorClusterListCommand) run(cctx *CommandContext, args []st
 	}
 	defer cl.Close()
 
+	// This is a listing command subject to json vs jsonl rules
+	cctx.Printer.StartList()
+	defer cctx.Printer.EndList()
+
 	var nextPageToken []byte
 	var execsProcessed int
 	for pageIndex := 0; ; pageIndex++ {
diff --git a/temporalcli/commands.workflow_view.go b/temporalcli/commands.workflow_view.go
index 94467eaa..145fa131 100644
--- a/temporalcli/commands.workflow_view.go
+++ b/temporalcli/commands.workflow_view.go
@@ -193,6 +193,10 @@ func (c *TemporalWorkflowListCommand) run(cctx *CommandContext, args []string) e
 	}
 	defer cl.Close()
 
+	// This is a listing command subject to json vs jsonl rules
+	cctx.Printer.StartList()
+	defer cctx.Printer.EndList()
+
 	// Build request and start looping. We always use default page size regardless
 	// of user-defined limit, because we're ok w/ extra page data and the default
 	// is not clearly defined.
diff --git a/temporalcli/commandsmd/commands.md b/temporalcli/commandsmd/commands.md
index 0ce1307f..1ac65eb2 100644
--- a/temporalcli/commandsmd/commands.md
+++ b/temporalcli/commandsmd/commands.md
@@ -52,7 +52,7 @@ This document has a specific structure used by a parser. Here are the rules:
 * `--env-file` (string) - File to read all environments (defaults to `$HOME/.config/temporalio/temporal.yaml`).
 * `--log-level` (string-enum) - Log level. Options: debug, info, warn, error, never. Default: info.
 * `--log-format` (string-enum) - Log format. Options: text, json. Default: text.
-* `--output`, `-o` (string-enum) - Data output format. Options: text, json. Default: text.
+* `--output`, `-o` (string-enum) - Data output format. Options: text, json, jsonl. Default: text.
 * `--time-format` (string-enum) - Time format. Options: relative, iso, raw. Default: relative.
 * `--color` (string-enum) - Set coloring. Options: always, never, auto. Default: auto.
 * `--no-json-shorthand-payloads` (bool) - Always all payloads as raw payloads even if they are JSON.
diff --git a/temporalcli/internal/printer/printer.go b/temporalcli/internal/printer/printer.go
index 180802c6..0412364d 100644
--- a/temporalcli/internal/printer/printer.go
+++ b/temporalcli/internal/printer/printer.go
@@ -21,14 +21,18 @@ type Colorer func(string, ...interface{}) string
 
 type Printer struct {
 	// Must always be present
-	Output               io.Writer
-	JSON                 bool
+	Output io.Writer
+	JSON   bool
+	// This is unset/empty in JSONL mode
 	JSONIndent           string
 	JSONPayloadShorthand bool
 	// Only used for non-JSON, defaults to RFC3339
 	FormatTime func(time.Time) string
 	// Only used for non-JSON, defaults to color.Magenta
 	TableHeaderColorer Colorer
+
+	listMode          bool
+	listModeFirstJSON bool // True until first JSON printed
 }
 
 // Ignored during JSON output
@@ -50,6 +54,42 @@ func (p *Printer) Printlnf(s string, v ...any) {
 	p.Println(fmt.Sprintf(s, v...))
 }
 
+// When called for JSON with indent, this will create an initial bracket and
+// make sure all [Printer.PrintStructured] calls get commas properly to appear
+// as a list (but the indention and multiline posture of the JSON remains). When
+// called for JSON without indent, this will make sure all
+// [Printer.PrintStructured] is on its own line (i.e. JSONL mode). When called
+// for non-JSON, this is a no-op.
+//
+// [Printer.EndList] must be called at the end. If this is called twice it will
+// panic. This and the end call are not safe for concurrent use.
+func (p *Printer) StartList() {
+	if p.listMode {
+		panic("already in list mode")
+	}
+	p.listMode, p.listModeFirstJSON = true, true
+	// Write initial bracket when non-jsonl
+	if p.JSON && p.JSONIndent != "" {
+		// Don't need newline, we count on initial object to do that
+		p.Output.Write([]byte("["))
+	}
+}
+
+// Must be called after [Printer.StartList] or will panic. See Godoc on that
+// function for more details.
+func (p *Printer) EndList() {
+	if !p.listMode {
+		panic("not in list mode")
+	}
+	p.listMode, p.listModeFirstJSON = false, false
+	// Write ending bracket when non-jsonl
+	if p.JSON && p.JSONIndent != "" {
+		// We prepend a newline because non-jsonl list mode doesn't do so after each
+		// line to help with commas
+		p.Output.Write([]byte("\n]\n"))
+	}
+}
+
 type StructuredOptions struct {
 	// Derived if not present. Ignored for JSON printing.
 	Fields []string
@@ -171,19 +211,40 @@ func (p *Printer) writef(s string, v ...any) {
 }
 
 func (p *Printer) printJSON(v any, options StructuredOptions) error {
+	// Before printing, if we're in non-jsonl list mode, we must append a comma
+	// and a newline if we're not the first JSON seen.
+	nonJSONLListMode := p.listMode && p.JSON && p.JSONIndent != ""
+	if nonJSONLListMode {
+		var prepend string
+		if p.listModeFirstJSON {
+			p.listModeFirstJSON = false
+			prepend = "\n"
+		} else {
+			prepend = ",\n"
+		}
+		if _, err := p.Output.Write([]byte(prepend)); err != nil {
+			return err
+		}
+	}
+
+	// Print JSON
 	shorthandPayloads := p.JSONPayloadShorthand
 	if options.OverrideJSONPayloadShorthand != nil {
 		shorthandPayloads = *options.OverrideJSONPayloadShorthand
 	}
-	b, err := p.jsonVal(v, p.JSONIndent, shorthandPayloads)
-	if err != nil {
+	if b, err := p.jsonVal(v, p.JSONIndent, shorthandPayloads); err != nil {
+		return err
+	} else if _, err := p.Output.Write(b); err != nil {
 		return err
 	}
-	_, err = p.Output.Write(b)
-	if err == nil {
-		_, err = p.Output.Write([]byte("\n"))
+
+	// Do not print a newline if in non-jsonl list mode
+	if !nonJSONLListMode {
+		if _, err := p.Output.Write([]byte("\n")); err != nil {
+			return err
+		}
 	}
-	return err
+	return nil
 }
 
 func (p *Printer) jsonVal(v any, indent string, shorthandPayloads bool) ([]byte, error) {
diff --git a/temporalcli/internal/printer/printer_test.go b/temporalcli/internal/printer/printer_test.go
index c52f0101..a5e7e1ee 100644
--- a/temporalcli/internal/printer/printer_test.go
+++ b/temporalcli/internal/printer/printer_test.go
@@ -15,7 +15,7 @@ import (
 // * Text printer specific and non-specific fields and all sorts of table options
 // * JSON printer
 
-func TestTextPrinter(t *testing.T) {
+func TestPrinter_Text(t *testing.T) {
 	type MyStruct struct {
 		Foo              string
 		Bar              bool
@@ -73,3 +73,70 @@ func normalizeMultiline(s string) string {
 	}
 	return ret
 }
+
+func TestPrinter_JSON(t *testing.T) {
+	var buf bytes.Buffer
+
+	// With indentation
+	p := printer.Printer{Output: &buf, JSON: true, JSONIndent: "  "}
+	p.Println("should not print")
+	require.NoError(t, p.PrintStructured(map[string]string{"foo": "bar"}, printer.StructuredOptions{}))
+	require.Equal(t, `{
+  "foo": "bar"
+}
+`, buf.String())
+
+	// Without indentation
+	buf.Reset()
+	p = printer.Printer{Output: &buf, JSON: true}
+	p.Println("should not print")
+	require.NoError(t, p.PrintStructured(map[string]string{"foo": "bar"}, printer.StructuredOptions{}))
+	require.Equal(t, "{\"foo\":\"bar\"}\n", buf.String())
+}
+
+func TestPrinter_JSONList(t *testing.T) {
+	var buf bytes.Buffer
+
+	// With indentation
+	p := printer.Printer{Output: &buf, JSON: true, JSONIndent: "  "}
+	p.StartList()
+	p.Println("should not print")
+	require.NoError(t, p.PrintStructured(map[string]string{"foo": "bar"}, printer.StructuredOptions{}))
+	require.NoError(t, p.PrintStructured(map[string]string{"baz": "qux"}, printer.StructuredOptions{}))
+	p.EndList()
+	require.Equal(t, `[
+{
+  "foo": "bar"
+},
+{
+  "baz": "qux"
+}
+]
+`, buf.String())
+
+	// Without indentation
+	buf.Reset()
+	p = printer.Printer{Output: &buf, JSON: true}
+	p.StartList()
+	p.Println("should not print")
+	require.NoError(t, p.PrintStructured(map[string]string{"foo": "bar"}, printer.StructuredOptions{}))
+	require.NoError(t, p.PrintStructured(map[string]string{"baz": "qux"}, printer.StructuredOptions{}))
+	p.EndList()
+	require.Equal(t, "{\"foo\":\"bar\"}\n{\"baz\":\"qux\"}\n", buf.String())
+
+	// Empty with indentation
+	buf.Reset()
+	p = printer.Printer{Output: &buf, JSON: true, JSONIndent: "  "}
+	p.StartList()
+	p.Println("should not print")
+	p.EndList()
+	require.Equal(t, "[\n]\n", buf.String())
+
+	// Empty without indentation
+	buf.Reset()
+	p = printer.Printer{Output: &buf, JSON: true}
+	p.StartList()
+	p.Println("should not print")
+	p.EndList()
+	require.Equal(t, "", buf.String())
+}