> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stagehand.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Logging

> Control terminal output and forward structured Stagehand logs

Configure logging through a single `logging` option on `Stagehand.create()`: a level, an output format, and a callback that receives each record. Use the language selector to see the exact shape your SDK accepts.

## Quick start

Choose your logging setup based on your environment:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import { localBrowser, Stagehand } from "@browserbasehq/stagehand";

    // Development: full debug output, human-readable
    const development = { level: "debug", format: "pretty" } as const;

    // Production: standard logging, machine-readable, forwarded to your platform
    const production = {
      level: "info",
      format: "json",
      onLog: yourProductionLogger, // Send to Sentry, DataDog, or similar
    } as const;

    // Testing: warnings and errors only, no console noise
    const testing = { level: "warn", onLog: yourTestLogger } as const;

    // Create one instance with the configuration for this environment
    const stagehand = await Stagehand.create({
      browser: await localBrowser.launch(),
      logging: development,
      // restOfYourConfiguration...
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from stagehand import Stagehand, local_browser

    # Development: full debug output, human-readable
    development = {"level": "debug", "format": "pretty"}

    # Production: standard logging, machine-readable, forwarded to your platform
    production = {
        "level": "info",
        "format": "json",
        "on_log": your_production_logger,  # Send to Sentry, DataDog, or similar
    }

    # Testing: warnings and errors only, no console noise
    testing = {"level": "warn", "on_log": your_test_logger}

    # Create one instance with the configuration for this environment
    browser = await local_browser.launch()

    stagehand = await Stagehand.create(
        browser=browser,
        logging=development,
        # rest_of_your_configuration...
    )
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    // Development: full debug output, human-readable
    development := &stagehand.StagehandClientLoggingConfig{
    	Level:  stagehand.StagehandClientLogLevelDebug,
    	Format: stagehand.StagehandClientLogFormatPretty,
    }

    // Production: standard logging, machine-readable, forwarded to your platform
    production := &stagehand.StagehandClientLoggingConfig{
    	Level:  stagehand.StagehandClientLogLevelInfo,
    	Format: stagehand.StagehandClientLogFormatJSON,
    	OnLog:  yourProductionLogger, // Send to Sentry, DataDog, or similar
    }

    // Testing: warnings and errors only, no console noise
    testing := &stagehand.StagehandClientLoggingConfig{
    	Level: stagehand.StagehandClientLogLevelWarn,
    	OnLog: yourTestLogger,
    }

    // Pick the configuration that matches the environment you are running in
    logging := development
    switch os.Getenv("APP_ENV") {
    case "production":
    	logging = production
    case "test":
    	logging = testing
    }

    browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
    if err != nil {
    	return err
    }
    defer func() { err = errors.Join(err, browser.Close(ctx)) }()

    client, err := stagehand.Create(ctx, stagehand.CreateOptions{
    	Browser: browser,
    	Logging: logging,
    	// restOfYourConfiguration...
    })
    if err != nil {
    	return err
    }
    defer func() { err = errors.Join(err, client.Close(ctx)) }()
    ```
  </Tab>
</Tabs>

***

## Operational logging

Real-time event logging during automation execution.

### Verbosity level

Control how much detail you see in logs. The level is always applied at the source, so the Stagehand runtime never generates or sends a record below your threshold. Every SDK sets that threshold the same way, and the default is `info`.

<Tabs>
  <Tab title="Level: debug">
    **Use for:** Development, debugging specific issues

    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        const stagehand = await Stagehand.create({
          browser,
          logging: { level: "debug" }, // Maximum detail
          // restOfYourConfiguration...
        });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        stagehand = await Stagehand.create(
            browser=browser,
            logging={"level": "debug"},  # Maximum detail
            # rest_of_your_configuration...
        )
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        client, err := stagehand.Create(ctx, stagehand.CreateOptions{
        	Browser: browser,
        	Logging: &stagehand.StagehandClientLoggingConfig{
        		Level: stagehand.StagehandClientLogLevelDebug, // Maximum detail
        	},
        	// restOfYourConfiguration...
        })
        if err != nil {
        	return err
        }

        defer func() { err = errors.Join(err, client.Close(ctx)) }()
        ```
      </Tab>
    </Tabs>

    <Accordion title="Example output">
      ```
      [stagehand] DEBUG Capturing DOM snapshot {"pageId":"page-1"}
      [stagehand] DEBUG DOM contains 847 elements {"count":847}
      [stagehand] DEBUG LLM inference started {"category":"llm"}
      [stagehand] DEBUG LLM response {"selector":"#btn-submit","method":"click"}
      [stagehand] INFO act completed successfully {"category":"action"}
      ```
    </Accordion>
  </Tab>

  <Tab title="Level: info (default)">
    **Use for:** Standard operations, staging, production

    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        const stagehand = await Stagehand.create({
          browser,
          logging: { level: "info" }, // Default level
          // restOfYourConfiguration...
        });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        stagehand = await Stagehand.create(
            browser=browser,
            logging={"level": "info"},  # Default level
            # rest_of_your_configuration...
        )
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        client, err := stagehand.Create(ctx, stagehand.CreateOptions{
        	Browser: browser,
        	Logging: &stagehand.StagehandClientLoggingConfig{
        		Level: stagehand.StagehandClientLogLevelInfo, // Default level
        	},
        	// restOfYourConfiguration...
        })
        if err != nil {
        	return err
        }

        defer func() { err = errors.Join(err, client.Close(ctx)) }()
        ```
      </Tab>
    </Tabs>

    <Accordion title="Example output">
      ```
      [stagehand] INFO act started {"category":"action"}
      [stagehand] INFO act completed successfully {"category":"action"}
      [stagehand] INFO extract started {"category":"extraction"}
      [stagehand] INFO extract completed {"category":"extraction"}
      ```
    </Accordion>
  </Tab>

  <Tab title="Level: error or off">
    **Use for:** Production with external monitoring, minimal noise, or handling secrets

    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        const errorsOnly = { level: "error" } as const; // Errors only
        const silent = { level: "off" } as const; // Nothing at all

        const stagehand = await Stagehand.create({
          browser,
          logging: errorsOnly,
          // restOfYourConfiguration...
        });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        errors_only = {"level": "error"}  # Errors only
        silent = {"level": "off"}  # Nothing at all

        stagehand = await Stagehand.create(
            browser=browser,
            logging=errors_only,
            # rest_of_your_configuration...
        )
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        // Errors only
        errorsOnly := &stagehand.StagehandClientLoggingConfig{
        	Level: stagehand.StagehandClientLogLevelError,
        }

        // Nothing at all
        silent := &stagehand.StagehandClientLoggingConfig{
        	Level: stagehand.StagehandClientLogLevelOff,
        }

        // Pick the configuration that matches how much output you want
        logging := errorsOnly
        if os.Getenv("STAGEHAND_SILENT") == "1" {
        	logging = silent
        }

        client, err := stagehand.Create(ctx, stagehand.CreateOptions{
        	Browser: browser,
        	Logging: logging,
        	// restOfYourConfiguration...
        })
        if err != nil {
        	return err
        }
        defer func() { err = errors.Join(err, client.Close(ctx)) }()
        ```
      </Tab>
    </Tabs>

    <Accordion title="Example output">
      ```
      [stagehand] ERROR act failed: element not found {"category":"action"}
      [stagehand] ERROR navigation timeout exceeded {"category":"navigation"}
      ```
    </Accordion>
  </Tab>
</Tabs>

<Warning>
  Turning logging off suppresses the record callback as well as console output. Do this when handling passwords or other secrets.
</Warning>

***

### Log destinations

Send logs to your console or through your own callback. The record callback drives every destination, so anything your own code can write to is a valid sink. To forward records to an observability platform, see [External logging platforms](#external-logging-platforms).

<Tabs>
  <Tab title="Pretty (default)">
    Human-readable, single-line console output written to standard error.

    **When to use:** Development and interactive debugging

    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        // Enabled by default: level "info", format "pretty"
        const stagehand = await Stagehand.create({
          browser,
          logging: { level: "info", format: "pretty" },
          // restOfYourConfiguration...
        });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        # Enabled by default: level "info", format "pretty"
        stagehand = await Stagehand.create(
            browser=browser,
            logging={"level": "info", "format": "pretty"},
            # rest_of_your_configuration...
        )
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        client, err := stagehand.Create(ctx, stagehand.CreateOptions{
        	Browser: browser,
        	Logging: &stagehand.StagehandClientLoggingConfig{
        		Level:  stagehand.StagehandClientLogLevelInfo,
        		Format: stagehand.StagehandClientLogFormatPretty,
        	},
        	// restOfYourConfiguration...
        })
        if err != nil {
        	return err
        }

        defer func() { err = errors.Join(err, client.Close(ctx)) }()
        ```
      </Tab>
    </Tabs>

    <Accordion title="Output shape">
      Each line is `[stagehand] LEVEL message {json data}`, with the data object omitted when empty. Stagehand writes it to standard error so it never mixes with your program's own standard output.
    </Accordion>
  </Tab>

  <Tab title="JSON">
    One JSON object per line, ready for log shippers and structured search.

    **When to use:** Containers, CI, or anywhere a collector tails standard error

    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        const stagehand = await Stagehand.create({
          browser,
          logging: { level: "info", format: "json" },
          // restOfYourConfiguration...
        });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        stagehand = await Stagehand.create(
            browser=browser,
            logging={"level": "info", "format": "json"},
            # rest_of_your_configuration...
        )
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        client, err := stagehand.Create(ctx, stagehand.CreateOptions{
        	Browser: browser,
        	Logging: &stagehand.StagehandClientLoggingConfig{
        		Level:  stagehand.StagehandClientLogLevelInfo,
        		Format: stagehand.StagehandClientLogFormatJSON,
        	},
        	// restOfYourConfiguration...
        })
        if err != nil {
        	return err
        }

        defer func() { err = errors.Join(err, client.Close(ctx)) }()
        ```
      </Tab>
    </Tabs>

    <Accordion title="Output shape">
      ```json theme={null}
      {"level":"info","message":"act completed successfully","data":{"category":"action"}}
      ```
    </Accordion>
  </Tab>

  <Tab title="Custom logger">
    Your own callback receives every log record that passes the level filter, alongside the console output.

    **When to use:** Development, debugging, or when you don't need querying
    capabilities.

    <Steps>
      <Step title="Create a simple logger">
        <Tabs>
          <Tab title="TypeScript">
            ```typescript theme={null}

            // Simple logger without parsing (for basic console output)
            // The callback receives a `StagehandLog`; see the reference below for the shape.
            const simpleLogger = (log: {
              level: "debug" | "info" | "warn" | "error";
              message: string;
              data: Record<string, unknown>;
            }) => {
              console.log(`[${log.level}] ${log.message}`);

              // Optional: log raw structured data
              if (Object.keys(log.data).length > 0) {
                console.log("  Context:", log.data);
              }
            };
            ```
          </Tab>

          <Tab title="Python">
            ```python theme={null}

            # Simple logger without parsing (for basic console output)
            def simple_logger(log) -> None:
                print(f"[{log.level.value}] {log.message}")

                # Optional: log raw structured data
                data = log.data.model_dump(mode="json")
                if data:
                    print("  Context:", data)
            ```
          </Tab>

          <Tab title="Go">
            ```go theme={null}
            // Simple logger without parsing (for basic console output)
            func simpleLogger(entry stagehand.StagehandLog) {
            	fmt.Printf("[%s] %s\n", entry.Level, entry.Message)

            	// Optional: log raw structured data
            	if len(entry.Data) > 0 {
            		fmt.Println("  Context:", entry.Data)
            	}
            }
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step title="Pass the logger in your Stagehand instance">
        Then pass the logger in your Stagehand instance:

        <Tabs>
          <Tab title="TypeScript">
            ```typescript theme={null}
            const stagehand = await Stagehand.create({
              browser: await localBrowser.launch(),
              logging: {
                level: "info",
                onLog: simpleLogger,
              },
              // restOfYourConfiguration...
            });
            ```
          </Tab>

          <Tab title="Python">
            ```python theme={null}
            browser = await local_browser.launch()

            stagehand = await Stagehand.create(
                browser=browser,
                logging={"level": "info", "on_log": simple_logger},
                # rest_of_your_configuration...
            )
            ```
          </Tab>

          <Tab title="Go">
            ```go theme={null}
            browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
            if err != nil {
            	return err
            }

            client, err := stagehand.Create(ctx, stagehand.CreateOptions{
            	Browser: browser,
            	Logging: &stagehand.StagehandClientLoggingConfig{
            		OnLog: simpleLogger,
            	},
            	// restOfYourConfiguration...
            })
            if err != nil {
            	return err
            }

            defer func() { err = errors.Join(err, client.Close(ctx)) }()
            ```
          </Tab>
        </Tabs>
      </Step>
    </Steps>

    <Note>
      The callback receives every record that passes the level gate, so do any finer filtering inside it. It runs in addition to the console output rather than instead of it: the level gates both and `off` silences both. Keep the level at the lowest severity you want to receive, and redirect standard error if the console itself needs to stay quiet.
    </Note>
  </Tab>
</Tabs>

***

### External logging platforms

The same log callback, pointed at an observability platform.

**When to use:** Production with Sentry, DataDog, CloudWatch, or a custom observability platform for centralized monitoring and error alerting.

#### Sentry

<Steps>
  <Step title="Create a production logger">
    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        import * as Sentry from "@sentry/node";

        const productionLogger = (log: {
          level: "debug" | "info" | "warn" | "error";
          message: string;
          data: Record<string, unknown>;
        }) => {
          // Send errors to Sentry
          if (log.level === "error") {
            Sentry.captureMessage(log.message, {
              level: "error",
              extra: log.data,
            });
          }
        };
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        import sentry_sdk

        def production_logger(log) -> None:
            # Send errors to Sentry
            if log.level.value == "error":
                sentry_sdk.capture_message(
                    log.message,
                    level="error",
                    extras=log.data.model_dump(mode="json"),
                )
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        func productionLogger(entry stagehand.StagehandLog) {
        	// Send errors to Sentry
        	if entry.Level != stagehand.StagehandLogLevelError {
        		return
        	}
        	sentry.WithScope(func(scope *sentry.Scope) {
        		// Data values are json.RawMessage. Pass them through so Sentry keeps each
        		// field's JSON type instead of recording it as quoted text.
        		for key, value := range entry.Data {
        			scope.SetExtra(key, value)
        		}
        		scope.SetLevel(sentry.LevelError)
        		sentry.CaptureMessage(entry.Message)
        	})
        }
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Pass the logger in your Stagehand instance">
    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        const stagehand = await Stagehand.create({
          browser: await localBrowser.launch(),
          logging: {
            level: "info",
            format: "json",
            onLog: productionLogger,
          },
          // restOfYourConfiguration...
        });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        browser = await local_browser.launch()

        stagehand = await Stagehand.create(
            browser=browser,
            logging={
                "level": "info",
                "format": "json",
                "on_log": production_logger,
            },
            # rest_of_your_configuration...
        )
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
        if err != nil {
        	return err
        }

        client, err := stagehand.Create(ctx, stagehand.CreateOptions{
        	Browser: browser,
        	Logging: &stagehand.StagehandClientLoggingConfig{
        		Level:  stagehand.StagehandClientLogLevelInfo,
        		Format: stagehand.StagehandClientLogFormatJSON,
        		OnLog:  productionLogger,
        	},
        	// restOfYourConfiguration...
        })
        if err != nil {
        	return err
        }

        defer func() { err = errors.Join(err, client.Close(ctx)) }()
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

#### DataDog

<Steps>
  <Step title="Create a production logger">
    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        import { datadogLogs } from "@datadog/browser-logs";

        const productionLogger = (log: {
          level: "debug" | "info" | "warn" | "error";
          message: string;
          data: Record<string, unknown>;
        }) => {
          // Send all logs to DataDog
          datadogLogs.logger.log(log.message, {
            status: log.level === "error" ? "error" : "info",
            service: "stagehand-automation",
            ...log.data,
          });
        };
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        from datadog_api_client import ApiClient, Configuration
        from datadog_api_client.v2.api.logs_api import LogsApi

        logs_api = LogsApi(ApiClient(Configuration()))

        def production_logger(log) -> None:
            # Send all logs to DataDog
            logs_api.submit_log(body=[{
                "message": log.message,
                "status": "error" if log.level.value == "error" else "info",
                "service": "stagehand-automation",
                **log.data.model_dump(mode="json"),
            }])
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        func productionLogger(entry stagehand.StagehandLog) {
        	// Send all logs to DataDog
        	status := "info"
        	if entry.Level == stagehand.StagehandLogLevelError {
        		status = "error"
        	}
        	payload := map[string]any{
        		"message": entry.Message,
        		"status":  status,
        		"service": "stagehand-automation",
        	}
        	// Data values are json.RawMessage. Keep them raw so DataDog indexes numbers
        	// as numbers and strings as strings.
        	for key, value := range entry.Data {
        		payload[key] = value
        	}
        	_ = submitDataDogLog(payload)
        }
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Pass the logger in your Stagehand instance">
    <Tabs>
      <Tab title="TypeScript">
        ```typescript theme={null}
        const stagehand = await Stagehand.create({
          browser: await localBrowser.launch(),
          logging: {
            level: "info",
            format: "json",
            onLog: productionLogger,
          },
          // restOfYourConfiguration...
        });
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        browser = await local_browser.launch()

        stagehand = await Stagehand.create(
            browser=browser,
            logging={
                "level": "info",
                "format": "json",
                "on_log": production_logger,
            },
            # rest_of_your_configuration...
        )
        ```
      </Tab>

      <Tab title="Go">
        ```go theme={null}
        browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
        if err != nil {
        	return err
        }

        client, err := stagehand.Create(ctx, stagehand.CreateOptions{
        	Browser: browser,
        	Logging: &stagehand.StagehandClientLoggingConfig{
        		Level:  stagehand.StagehandClientLogLevelInfo,
        		Format: stagehand.StagehandClientLogFormatJSON,
        		OnLog:  productionLogger,
        	},
        	// restOfYourConfiguration...
        })
        if err != nil {
        	return err
        }

        defer func() { err = errors.Join(err, client.Close(ctx)) }()
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

<Note>
  Failures inside your callback are caught and reported on standard error, so a failing logger will not take down your automation. That covers exceptions and rejected promises in TypeScript and Python, and a panic in Go.
</Note>

***

## File-based session logging

Route the log callback to a file to get a durable, per-session record of every Stagehand operation: `act`, `observe`, `extract`, LLM inference, and browser events.

### Setup

Pick a directory for your session logs and open a file per run:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import { createWriteStream } from "node:fs";
    import { mkdirSync } from "node:fs";

    mkdirSync("./stagehand-logs", { recursive: true });
    const sessionId = new Date().toISOString().replace(/[:.]/g, "-");
    const logFile = createWriteStream(`./stagehand-logs/${sessionId}.jsonl`, { flags: "a" });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from datetime import datetime, timezone
    from pathlib import Path

    Path("./stagehand-logs").mkdir(parents=True, exist_ok=True)
    session_id = datetime.now(timezone.utc).isoformat().replace(":", "-").replace(".", "-")
    log_file = open(f"./stagehand-logs/{session_id}.jsonl", "a", encoding="utf-8")
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    if err := os.MkdirAll("./stagehand-logs", 0o755); err != nil {
    	return err
    }
    startedAt := time.Now().UTC()
    sessionID := strings.NewReplacer(":", "-", ".", "-").Replace(startedAt.Format(time.RFC3339Nano))
    logFile, err := os.OpenFile(
    	fmt.Sprintf("./stagehand-logs/%s.jsonl", sessionID),
    	os.O_APPEND|os.O_CREATE|os.O_WRONLY,
    	0o600,
    )
    if err != nil {
    	return err
    }
    fmt.Fprintf(os.Stderr, "writing Stagehand logs to %s\n", logFile.Name())
    ```
  </Tab>
</Tabs>

### Usage

Write each record as one JSON line, then run your Stagehand script as normal:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import { finished } from "node:stream/promises";
    import { localBrowser, Stagehand } from "@browserbasehq/stagehand";

    const stagehand = await Stagehand.create({
      browser: await localBrowser.launch(),
      logging: {
        level: "debug",
        format: "pretty",
        onLog(log) {
          logFile.write(`${JSON.stringify(log)}\n`);
        },
      },
    });

    try {
      // ... your automation
    } finally {
      await stagehand.close();
      logFile.end();
      await finished(logFile);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from stagehand import Stagehand, local_browser

    browser = await local_browser.launch()

    stagehand = await Stagehand.create(
        browser=browser,
        logging={
            "level": "debug",
            "format": "pretty",
            "on_log": lambda log: print(log.model_dump_json(), file=log_file),
        },
    )

    try:
        ...  # your automation
    finally:
        await stagehand.close()
        log_file.close()
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    defer func() { err = errors.Join(err, logFile.Close()) }()

    client, err := stagehand.Create(ctx, stagehand.CreateOptions{
    	Browser: browser,
    	Logging: &stagehand.StagehandClientLoggingConfig{
    		Level:  stagehand.StagehandClientLogLevelDebug,
    		Format: stagehand.StagehandClientLogFormatPretty,
    		OnLog: func(log stagehand.StagehandLog) {
    			_ = json.NewEncoder(logFile).Encode(log)
    		},
    	},
    })
    if err != nil {
    	return err
    }
    defer func() { err = errors.Join(err, client.Close(ctx)) }()

    // ... your automation
    ```
  </Tab>
</Tabs>

### Viewing logs

<Tabs>
  <Tab title="Real-time monitoring">
    Follow all logs as they happen:

    ```bash theme={null}
    tail -f ./stagehand-logs/*.jsonl
    ```

    Or filter by category as they stream:

    ```bash theme={null}
    # LLM requests and responses only
    tail -f ./stagehand-logs/*.jsonl | jq 'select(.data.category == "llm")'

    # Action events only
    tail -f ./stagehand-logs/*.jsonl | jq 'select(.data.category == "action")'
    ```
  </Tab>

  <Tab title="Chronological review">
    View unified output across every session file:

    ```bash theme={null}
    cat ./stagehand-logs/*.jsonl | jq -r '"\(.level)\t\(.message)"'
    ```
  </Tab>

  <Tab title="Historical sessions">
    Browse previous session logs:

    ```bash theme={null}
    ls ./stagehand-logs/
    # Output: 2026-01-06T14-30-45-000Z.jsonl  2026-01-06T15-45-12-000Z.jsonl

    jq -r 'select(.level == "error")' ./stagehand-logs/2026-01-06T14-30-45-000Z.jsonl
    ```
  </Tab>
</Tabs>

### Log files

Because you own the sink, you decide how records are split. A common layout is one file per concern, keyed off the log category:

| File                   | Contents                                                            |
| ---------------------- | ------------------------------------------------------------------- |
| `llm_events.jsonl`     | LLM requests and responses for act, extract, and observe operations |
| `browser_events.jsonl` | Navigation, snapshot, and page lifecycle events                     |
| `stagehand.jsonl`      | Every record, unfiltered                                            |

<Note>
  This is especially useful for debugging long workflows where you need to trace the full sequence of LLM decisions and browser actions after the fact.
</Note>

***

## LLM inference debugging

<Warning>
  **Development only** - Produces large volumes of output and contains page content. Do not use in production.
</Warning>

Run at the `debug` level to see the complete inference path: the snapshot that was captured, the prompt category, the model's chosen element, and the token counts for each call.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const stagehand = await Stagehand.create({
      browser: await localBrowser.launch(),
      logging: {
        level: "debug",
        format: "json",
        onLog(log) {
          // Persist only the LLM records for offline analysis
          if (log.data.category === "llm") {
            inferenceFile.write(`${JSON.stringify(log)}\n`);
          }
        },
      },
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    def capture_inference(log) -> None:
        # Persist only the LLM records for offline analysis
        data = log.data.model_dump(mode="json")
        if data.get("category") == "llm":
            print(log.model_dump_json(), file=inference_file)

    browser = await local_browser.launch()

    stagehand = await Stagehand.create(
        browser=browser,
        logging={
            "level": "debug",
            "format": "json",
            "on_log": capture_inference,
        },
    )
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    func captureInference(inferenceFile *os.File) func(stagehand.StagehandLog) {
    	return func(entry stagehand.StagehandLog) {
    		// Persist only the LLM records for offline analysis
    		if category, ok := entry.Data["category"]; ok && string(category) == `"llm"` {
    			_ = json.NewEncoder(inferenceFile).Encode(entry)
    		}
    	}
    }

    browser, err := stagehand.LaunchLocalBrowser(ctx, &stagehand.LocalBrowserLaunchOptions{})
    if err != nil {
    	return err
    }

    client, err := stagehand.Create(ctx, stagehand.CreateOptions{
    	Browser: browser,
    	Logging: &stagehand.StagehandClientLoggingConfig{
    		Level: stagehand.StagehandClientLogLevelDebug,
    		OnLog: captureInference(inferenceFile),
    	},
    })
    if err != nil {
    	return err
    }

    defer func() { err = errors.Join(err, client.Close(ctx)) }()
    ```
  </Tab>
</Tabs>

Debug-level records include:

<AccordionGroup>
  <Accordion title="Inference request">
    Emitted before each LLM call, carrying the operation and prompt metadata:

    ```json theme={null}
    {
      "level": "debug",
      "message": "inference started",
      "data": {
        "category": "llm",
        "operation": "act",
        "model": "openai/gpt-5.4-mini"
      }
    }
    ```
  </Accordion>

  <Accordion title="Inference response">
    Emitted when the model returns, carrying the selected element:

    ```json theme={null}
    {
      "level": "debug",
      "message": "inference completed",
      "data": {
        "category": "llm",
        "operation": "act",
        "elementId": "0-1183",
        "method": "click"
      }
    }
    ```
  </Accordion>

  <Accordion title="Usage summary">
    Emitted once per operation with the token and latency accounting:

    ```json theme={null}
    {
      "level": "debug",
      "message": "act inference usage",
      "data": {
        "category": "llm",
        "promptTokens": 3451,
        "completionTokens": 45,
        "inferenceTimeMs": 951
      }
    }
    ```
  </Accordion>
</AccordionGroup>

<Tip>
  Token counters are also aggregated across the whole session. See [Observability](/v4/configuration/observability#real-time-metrics-%26-monitoring) for the cumulative view.
</Tip>

***

## Reference

### Logging configuration

Pass all logging options as a single object to `Stagehand.create()`:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    // The shape of the `logging` value in the Stagehand.create() options.
    type StagehandClientLoggingConfig = {
      level?: "off" | "error" | "warn" | "info" | "debug"; // default: "info"
      format?: "pretty" | "json"; // default: "pretty"
      onLog?: (log: StagehandLog) => void | Promise<void>; // default: undefined
    };
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    stagehand = await Stagehand.create(
        browser=browser,
        # ... your other configurations (browser, model, etc.)
        logging={
            "level": "info",  # "off" | "error" | "warn" | "info" | "debug"
            "format": "pretty",  # "pretty" | "json"
            "on_log": None,  # Callable[[StagehandLog], None | Awaitable[None]]
        },
    )
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    client, err := stagehand.Create(ctx, stagehand.CreateOptions{
    	Browser: browser,
    	Logging: &stagehand.StagehandClientLoggingConfig{
    		Level:  stagehand.StagehandClientLogLevelInfo,
    		Format: stagehand.StagehandClientLogFormatPretty,
    		OnLog:  func(entry stagehand.StagehandLog) {},
    	},
    	// ... your other configurations (Browser, Model, etc.)

    })
    if err != nil {
    	return err
    }

    defer func() { err = errors.Join(err, client.Close(ctx)) }()
    ```
  </Tab>
</Tabs>

| Option       | Default  | Description                                                                      |
| ------------ | -------- | -------------------------------------------------------------------------------- |
| level        | `info`   | Minimum level to emit: `off`, `error`, `warn`, `info`, or `debug`                |
| format       | `pretty` | Console rendering: human-readable lines or one JSON object per line              |
| log callback | none     | Receives every record that passes the level filter, alongside the console output |

<Note>
  The level is also forwarded to the Stagehand runtime, so suppressed records are never generated or sent over the wire. Raising it to `debug` increases both log volume and message traffic.
</Note>

### Log structure

Each log entry follows a structured format:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    interface StagehandLog {
      level: "debug" | "info" | "warn" | "error"; // Severity
      message: string;                            // "act completed successfully"
      data: Record<string, unknown>;              // Structured metadata, JSON-serializable
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    class StagehandLog(BaseModel):
        level: StagehandLogLevel  # "debug" | "info" | "warn" | "error"
        message: str              # "act completed successfully"
        data: StagehandLogData    # Structured metadata, JSON-serializable
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    type StagehandLog struct {
    	Level   StagehandLogLevel // "debug" | "info" | "warn" | "error"
    	Message string            // "act completed successfully"
    	Data    StagehandLogData  // map[string]json.RawMessage
    }
    ```
  </Tab>
</Tabs>

<Note>
  Unlike v3's `LogLine`, `data` is a flat JSON object: values are already typed, so there is no `{ value, type }` wrapper to parse. Fields such as `category` and `timestamp` appear inside `data` when the emitting operation provides them.
</Note>

<Accordion title="Log examples">
  <Tabs>
    <Tab title="Successful action">
      ```json theme={null}
      {
        "level": "info",
        "message": "act completed successfully",
        "data": {
          "category": "action",
          "selector": "xpath=/html[1]/body[1]/button[1]",
          "executionTimeMs": 1250
        }
      }
      ```
    </Tab>

    <Tab title="LLM inference">
      ```json theme={null}
      {
        "level": "debug",
        "message": "inference completed",
        "data": {
          "category": "llm",
          "model": "openai/gpt-5.4-mini",
          "promptTokens": 3451,
          "completionTokens": 45
        }
      }
      ```
    </Tab>

    <Tab title="Error">
      ```json theme={null}
      {
        "level": "error",
        "message": "action failed: element not found",
        "data": {
          "category": "action",
          "selector": "#missing-btn",
          "url": "https://example.com/form"
        }
      }
      ```
    </Tab>
  </Tabs>
</Accordion>

***

## Next steps

Now that you have logging configured, explore additional debugging and monitoring tools in [the Observability guide](/v4/configuration/observability):

<CardGroup cols={2}>
  <Card title="Tracing" icon="diagram-project" href="/v4/configuration/observability#tracing">
    Export OpenTelemetry spans for every Stagehand operation to your own collector, with trace context propagated across the SDK and runtime boundary.
  </Card>

  <Card title="Metrics API" icon="chart-line" href="/v4/configuration/observability#real-time-metrics-%26-monitoring">
    Monitor token usage and performance in real-time. Track costs per operation, identify expensive calls, and optimize resource usage.
  </Card>

  <Card title="LLM inference debugging" icon="microscope" href="/v4/configuration/logging#llm-inference-debugging">
    Run at the debug level to see exactly what Stagehand sent to the model and why the model chose each action.
  </Card>

  <Card title="Browserbase session monitoring" icon="video" href="/v4/configuration/observability#browserbase-session-monitoring">
    Watch your automation visually with session recordings, network monitoring, and real-time browser inspection (Browserbase only).
  </Card>
</CardGroup>
