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

# Execution Logs

> Stream logs in real-time or retrieve logs from completed executions

<Tip>Start with the [Runloop Quickstart](/docs/tutorials/quickstart) to use the examples below.</Tip>

## Overview

When executing commands on a Devbox, you have two primary ways to access logs:

1. **Log Streaming**: Receive stdout and stderr output in real-time as the command runs
2. **Completed Execution Logs**: Retrieve the full output after a command has finished

## Real-Time Log Streaming

For long-running commands like builds, tests, or servers, you can stream output as it's generated using callback functions.

### Streaming Combined Output

Use the `output` callback to receive both stdout and stderr combined:

<CodeGroup>
  ```python Python theme={null}
  command = await devbox.cmd.exec_async(
      "npm run build",
      output=lambda line: print(f"[LOG] {line}")
  )
  ```

  ```typescript TypeScript theme={null}
  const command = await devbox.cmd.execAsync(
      "npm run build",
      { output: (line: string) => console.log(`[LOG] ${line}`) }
  );
  ```
</CodeGroup>

### Streaming Stdout and Stderr Separately

For more control, use separate callbacks for stdout and stderr:

<CodeGroup>
  ```python Python theme={null}
  command = await devbox.cmd.exec_async(
      "npm run build",
      stdout=lambda line: print(f"[STDOUT] {line}"),
      stderr=lambda line: print(f"[STDERR] {line}")
  )
  ```

  ```typescript TypeScript theme={null}
  const command = await devbox.cmd.execAsync(
      "npm run build",
      {
          stdout: (line: string) => console.log(`[STDOUT] ${line}`),
          stderr: (line: string) => console.error(`[STDERR] ${line}`)
      }
  );
  ```
</CodeGroup>

<Note>
  Streaming is ideal for commands where you want immediate feedback, such as build processes, test suites, or log tailing.
</Note>

<Note>
  By default, commands execute in the `/home/user` directory. Use [Named Shells](/docs/devboxes/named-shells) to maintain a working directory across multiple commands.
</Note>

### Streaming with Named Shells

Named shells also support log streaming:

<CodeGroup>
  ```python Python theme={null}
  shell = devbox.shell("build-session")

  result = await shell.exec(
      "npm install && npm run build",
      stdout=lambda line: print(f"[BUILD] {line}"),
      stderr=lambda line: print(f"[ERROR] {line}")
  )
  ```

  ```typescript TypeScript theme={null}
  const shell = devbox.shell('build-session');

  const result = await shell.exec(
      'npm install && npm run build',
      {
          stdout: (line: string) => console.log(`[BUILD] ${line}`),
          stderr: (line: string) => console.error(`[ERROR] ${line}`)
      }
  );
  ```
</CodeGroup>

## Getting Logs from Completed Executions

After a command finishes, you can retrieve the complete stdout and stderr output, or just the last N lines.

### Retrieving Full Output

<CodeGroup>
  ```python Python theme={null}
  result = await devbox.cmd.exec("npm run test")

  # Get full stdout
  stdout = await result.stdout()
  print(f"Test output:\n{stdout}")

  # Get full stderr
  stderr = await result.stderr()
  if stderr:
      print(f"Errors:\n{stderr}")

  # Check exit code
  exit_code = result.exit_code
  print(f"Exit code: {exit_code}")
  ```

  ```typescript TypeScript theme={null}
  const result = await devbox.cmd.exec("npm run test");

  // Get full stdout
  const stdout = await result.stdout();
  console.log(`Test output:\n${stdout}`);

  // Get full stderr
  const stderr = await result.stderr();
  if (stderr) {
      console.log(`Errors:\n${stderr}`);
  }

  // Check exit code
  const exitCode = result.exitCode;
  console.log(`Exit code: ${exitCode}`);
  ```
</CodeGroup>

### Retrieving Last N Lines

When you only need the most recent logs, you can specify the number of lines to retrieve. This is more efficient as it avoids fetching the entire output:

<CodeGroup>
  ```python Python theme={null}
  result = await devbox.cmd.exec("npm run test")

  # Get last 10 lines of stdout
  last_lines = await result.stdout(10)
  print(f"Last 10 lines:\n{last_lines}")

  # Get last 5 lines of stderr
  recent_errors = await result.stderr(5)
  if recent_errors:
      print(f"Recent errors:\n{recent_errors}")
  ```

  ```typescript TypeScript theme={null}
  const result = await devbox.cmd.exec("npm run test");

  // Get last 10 lines of stdout
  const lastLines = await result.stdout(10);
  console.log(`Last 10 lines:\n${lastLines}`);

  // Get last 5 lines of stderr
  const recentErrors = await result.stderr(5);
  if (recentErrors) {
      console.log(`Recent errors:\n${recentErrors}`);
  }
  ```
</CodeGroup>

<Note>
  Specifying `numLines` resolves faster than fetching all logs, especially for commands with verbose output. Use this when you only need to check the final status or recent errors.
</Note>

### Retrieving Logs from Async Executions

For commands started with `exec_async`, you can wait for completion and then retrieve logs:

<CodeGroup>
  ```python Python theme={null}
  # Start a long-running command
  command = await devbox.cmd.exec_async("npm run build")

  # Wait for the command to complete
  result = await command.result()

  # Get the complete output
  stdout = await result.stdout()
  stderr = await result.stderr()
  exit_code = result.exit_code

  print(f"Build completed with exit code: {exit_code}")
  print(f"Output:\n{stdout}")
  if stderr:
      print(f"Errors:\n{stderr}")
  ```

  ```typescript TypeScript theme={null}
  // Start a long-running command
  const command = await devbox.cmd.execAsync("npm run build");

  // Wait for the command to complete
  const result = await command.result();

  // Get the complete output
  const stdout = await result.stdout();
  const stderr = await result.stderr();
  const exitCode = result.exitCode;

  console.log(`Build completed with exit code: ${exitCode}`);
  console.log(`Output:\n${stdout}`);
  if (stderr) {
      console.log(`Errors:\n${stderr}`);
  }
  ```
</CodeGroup>

## Combining Streaming and Final Logs

You can stream logs in real-time while still having access to the complete output after execution:

<CodeGroup>
  ```python Python theme={null}
  # Stream logs while the command runs
  result = await devbox.cmd.exec(
      "npm run test",
      stdout=lambda line: print(f"[LIVE] {line}")
  )

  # After completion, access the full output
  full_stdout = await result.stdout()
  exit_code = result.exit_code

  # Process or store the complete logs
  if exit_code != 0:
      save_failure_logs(full_stdout)
  ```

  ```typescript TypeScript theme={null}
  // Stream logs while the command runs
  const result = await devbox.cmd.exec(
      "npm run test",
      { stdout: (line: string) => console.log(`[LIVE] ${line}`) }
  );

  // After completion, access the full output
  const fullStdout = await result.stdout();
  const exitCode = result.exitCode;

  // Process or store the complete logs
  if (exitCode !== 0) {
      saveFailureLogs(fullStdout);
  }
  ```
</CodeGroup>

## Log Retention

Execution logs are retained for **28 days** after the command completes. During this window, you can retrieve logs for any past execution — even if the devbox has been shut down. After 28 days, logs are automatically deleted.

### Downloading Logs for a Devbox

You can download all execution logs for a devbox using the SDK or CLI:

<CodeGroup>
  ```python Python theme={null}
  devbox = await runloop.devbox.from_id("dvb_123")
  logs = await devbox.logs()
  for log in logs.logs:
      print(f"[{log.timestamp_ms}] {log.level}: {log.message}")
  ```

  ```typescript TypeScript theme={null}
  const devbox = await runloop.devbox.fromId("dvb_123");
  const logs = await devbox.logs();
  for (const log of logs.logs) {
      console.log(`[${log.timestampMs}] ${log.level}: ${log.message}`);
  }
  ```

  ```bash CLI theme={null}
  rli devbox logs <devbox_id>
  ```
</CodeGroup>

<Note>
  If you need logs beyond the 28-day retention window, download and store them externally before they expire. There is no bulk log download across multiple devboxes — use `devbox list` with filters and download logs for each devbox individually.
</Note>

## Best Practices

### Use Streaming for Long-Running Commands

For commands that take more than a few seconds, streaming provides immediate feedback:

<CodeGroup>
  ```python Python theme={null}
  # Good: Stream output for long builds
  await devbox.cmd.exec(
      "npm install",
      output=lambda line: print(line)
  )

  # Less ideal: Wait for all output at once
  result = await devbox.cmd.exec("npm install")
  print(await result.stdout())  # No feedback until complete
  ```

  ```typescript TypeScript theme={null}
  // Good: Stream output for long builds
  await devbox.cmd.exec(
      "npm install",
      { output: (line: string) => console.log(line) }
  );

  // Less ideal: Wait for all output at once
  const result = await devbox.cmd.exec("npm install");
  console.log(await result.stdout());  // No feedback until complete
  ```
</CodeGroup>

### Store Logs for Debugging

When running automated workflows, store logs for later analysis:

<CodeGroup>
  ```python Python theme={null}
  import json
  from datetime import datetime

  async def run_with_logging(devbox, command):
      logs = []
      
      result = await devbox.cmd.exec(
          command,
          stdout=lambda line: logs.append({"stream": "stdout", "line": line}),
          stderr=lambda line: logs.append({"stream": "stderr", "line": line})
      )
      
      return {
          "command": command,
          "exit_code": result.exit_code,
          "logs": logs,
          "timestamp": datetime.now().isoformat()
      }
  ```

  ```typescript TypeScript theme={null}
  interface LogEntry {
      stream: 'stdout' | 'stderr';
      line: string;
  }

  async function runWithLogging(devbox: Devbox, command: string) {
      const logs: LogEntry[] = [];
      
      const result = await devbox.cmd.exec(command, {
          stdout: (line: string) => logs.push({ stream: 'stdout', line }),
          stderr: (line: string) => logs.push({ stream: 'stderr', line })
      });
      
      return {
          command,
          exitCode: result.exitCode,
          logs,
          timestamp: new Date().toISOString()
      };
  }
  ```
</CodeGroup>

### Handle Errors Gracefully

Use the `success` or `failed` properties to check execution status, or check the exit code directly:

<CodeGroup>
  ```python Python theme={null}
  result = await devbox.cmd.exec("npm run build")

  exit_code = result.exit_code
  if exit_code != 0:
      stderr = await result.stderr()
      stdout = await result.stdout()
      raise BuildError(
          f"Build failed with exit code {exit_code}\n"
          f"stderr: {stderr}\n"
          f"stdout: {stdout}"
      )
  ```

  ```typescript TypeScript theme={null}
  const result = await devbox.cmd.exec("npm run build");

  // Use the success/failed properties for convenience
  if (result.failed) {
      const stderr = await result.stderr();
      const stdout = await result.stdout();
      throw new Error(
          `Build failed with exit code ${result.exitCode}\n` +
          `stderr: ${stderr}\n` +
          `stdout: ${stdout}`
      );
  }

  // Or check for success
  if (result.success) {
      console.log("Build completed successfully!");
  }
  ```
</CodeGroup>

<Note>
  For more information about command execution, see the [Execute Commands](/docs/devboxes/execute-commands) documentation. For stateful shell sessions, see [Named Shells](/docs/devboxes/named-shells).
</Note>
