Back to Blog
DevOps
4 min read

Improving Error Output in DevOps Pipeline Emails

Azure DevOpsEmailNotificationsDebuggingAutomation

When a pipeline fails, the default notification says "Build Failed" with a link. You want to know why it failed without clicking through. Here's how to capture and include error details in notifications.

Capturing Error Output

Store Task Output in Variables

- task: PowerShell@2
  name: RunTests
  displayName: 'Run Tests'
  continueOnError: true
  inputs:
    targetType: 'inline'
    script: |
      try {
        $output = npm test 2>&1
        Write-Host $output
        echo "##vso[task.setvariable variable=TestOutput;isOutput=true]$($output -join "`n")"
        echo "##vso[task.setvariable variable=TestResult;isOutput=true]Success"
      }
      catch {
        $errorMsg = $_.Exception.Message
        Write-Host "##vso[task.logissue type=error]$errorMsg"
        echo "##vso[task.setvariable variable=TestOutput;isOutput=true]$errorMsg"
        echo "##vso[task.setvariable variable=TestResult;isOutput=true]Failed"
        exit 1
      }

Capture Build Logs

- task: PowerShell@2
  displayName: 'Capture Build Errors'
  condition: failed()
  inputs:
    targetType: 'inline'
    script: |
      $org = "$(System.CollectionUri)".TrimEnd('/')
      $project = "$(System.TeamProject)"
      $buildId = "$(Build.BuildId)"
      $pat = "$(System.AccessToken)"

      $base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
      $headers = @{ Authorization = "Basic $base64Auth" }

      # Get timeline (contains all task results)
      $timelineUrl = "$org/$project/_apis/build/builds/$buildId/timeline?api-version=7.0"
      $timeline = Invoke-RestMethod -Uri $timelineUrl -Headers $headers

      # Find failed tasks
      $failedTasks = $timeline.records |
        Where-Object { $_.result -eq "failed" -and $_.type -eq "Task" } |
        Select-Object name, @{N='issues';E={$_.issues | ForEach-Object { $_.message }}}

      $errorSummary = $failedTasks | ForEach-Object {
        "Task: $($_.name)`n$($_.issues -join "`n")"
      }

      echo "##vso[task.setvariable variable=ErrorSummary]$($errorSummary -join "`n`n")"

Sending Detailed Failure Notifications

- task: PowerShell@2
  displayName: 'Send Failure Email'
  condition: failed()
  inputs:
    targetType: 'inline'
    script: |
      $errorDetails = "$(ErrorSummary)"
      if ([string]::IsNullOrEmpty($errorDetails)) {
        $errorDetails = "No specific error captured. Check build logs."
      }

      $htmlBody = @"
      <html>
      <body style="font-family: Arial, sans-serif;">
        <h2 style="color: #dc3545;">Pipeline Failed</h2>

        <table style="border-collapse: collapse; margin: 20px 0;">
          <tr>
            <td style="padding: 8px; border: 1px solid #ddd;"><strong>Pipeline</strong></td>
            <td style="padding: 8px; border: 1px solid #ddd;">$(Build.DefinitionName)</td>
          </tr>
          <tr>
            <td style="padding: 8px; border: 1px solid #ddd;"><strong>Build</strong></td>
            <td style="padding: 8px; border: 1px solid #ddd;">$(Build.BuildNumber)</td>
          </tr>
          <tr>
            <td style="padding: 8px; border: 1px solid #ddd;"><strong>Branch</strong></td>
            <td style="padding: 8px; border: 1px solid #ddd;">$(Build.SourceBranchName)</td>
          </tr>
          <tr>
            <td style="padding: 8px; border: 1px solid #ddd;"><strong>Triggered By</strong></td>
            <td style="padding: 8px; border: 1px solid #ddd;">$(Build.RequestedFor)</td>
          </tr>
        </table>

        <h3>Error Details</h3>
        <pre style="background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto;">
$($errorDetails)
        </pre>

        <p>
          <a href="$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)"
             style="background: #0078d4; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
            View Build Logs
          </a>
        </p>
      </body>
      </html>
      "@

      $body = @{
        personalizations = @(
          @{
            to = @(@{ email = "$(FailureNotificationEmail)" })
            subject = "FAILED: $(Build.DefinitionName) #$(Build.BuildNumber)"
          }
        )
        from = @{ email = "[email protected]"; name = "Azure DevOps" }
        content = @(@{ type = "text/html"; value = $htmlBody })
      } | ConvertTo-Json -Depth 10

      Invoke-RestMethod -Uri "https://api.sendgrid.com/v3/mail/send" `
        -Method Post `
        -Headers @{ "Authorization" = "Bearer $(SendGridApiKey)" } `
        -ContentType "application/json" `
        -Body $body

Including Test Results

- task: PublishTestResults@2
  inputs:
    testResultsFormat: 'JUnit'
    testResultsFiles: '**/test-results.xml'

- task: PowerShell@2
  displayName: 'Get Test Summary'
  inputs:
    targetType: 'inline'
    script: |
      $org = "$(System.CollectionUri)".TrimEnd('/')
      $project = "$(System.TeamProject)"
      $buildId = "$(Build.BuildId)"
      $pat = "$(System.AccessToken)"

      $base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
      $headers = @{ Authorization = "Basic $base64Auth" }

      # Get test runs
      $runsUrl = "$org/$project/_apis/test/runs?buildId=$buildId&api-version=7.0"
      $runs = Invoke-RestMethod -Uri $runsUrl -Headers $headers

      $summary = foreach ($run in $runs.value) {
        $resultsUrl = "$org/$project/_apis/test/runs/$($run.id)/results?api-version=7.0"
        $results = Invoke-RestMethod -Uri $resultsUrl -Headers $headers

        $failed = $results.value | Where-Object { $_.outcome -eq "Failed" }

        if ($failed) {
          foreach ($test in $failed) {
            "FAILED: $($test.testCaseTitle)`n$($test.errorMessage)"
          }
        }
      }

      echo "##vso[task.setvariable variable=FailedTests]$($summary -join "`n`n")"

Slack/Teams Alternative

For faster notification, use webhooks:

Microsoft Teams

- task: PowerShell@2
  condition: failed()
  inputs:
    targetType: 'inline'
    script: |
      $webhookUrl = "$(TeamsWebhookUrl)"

      $body = @{
        "@type" = "MessageCard"
        "@context" = "http://schema.org/extensions"
        themeColor = "dc3545"
        summary = "Build Failed"
        sections = @(
          @{
            activityTitle = "Pipeline Failed: $(Build.DefinitionName)"
            facts = @(
              @{ name = "Build"; value = "$(Build.BuildNumber)" }
              @{ name = "Branch"; value = "$(Build.SourceBranchName)" }
              @{ name = "Error"; value = "$(ErrorSummary)" }
            )
            markdown = $true
          }
        )
        potentialAction = @(
          @{
            "@type" = "OpenUri"
            name = "View Build"
            targets = @(
              @{
                os = "default"
                uri = "$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)"
              }
            )
          }
        )
      } | ConvertTo-Json -Depth 10

      Invoke-RestMethod -Uri $webhookUrl -Method Post -Body $body -ContentType "application/json"

Notification Template

Store in repo for consistency:

<!-- templates/failure-email.html -->
<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; }
    .header { background: #dc3545; color: white; padding: 20px; }
    .content { padding: 20px; }
    .error-box { background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px; }
    .button { background: #0078d4; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; }
    table { width: 100%; border-collapse: collapse; margin: 15px 0; }
    td { padding: 8px; border: 1px solid #ddd; }
  </style>
</head>
<body>
  <div class="header">
    <h2>{{PipelineName}} - Build Failed</h2>
  </div>
  <div class="content">
    <table>
      <tr><td><strong>Build</strong></td><td>{{BuildNumber}}</td></tr>
      <tr><td><strong>Branch</strong></td><td>{{Branch}}</td></tr>
      <tr><td><strong>Triggered By</strong></td><td>{{RequestedFor}}</td></tr>
    </table>

    <h3>Error Summary</h3>
    <div class="error-box">
      <pre>{{ErrorDetails}}</pre>
    </div>

    <p><a href="{{BuildUrl}}" class="button">View Build Logs</a></p>
  </div>
</body>
</html>

Need help improving your DevOps workflows? Get in touch - we help teams build better CI/CD processes.

Need help with your Azure environment?

Get in touch for a free consultation.

Get in Touch