GitHub Actions can automatically generate job summaries, but sometimes you need more control over what appears in the summary to make it more useful.

Let’s see how a custom summary looks in practice. Imagine you have a workflow that builds and tests your application.

name: Build and Test

on: [push]

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        id: run_tests
        run: |
          npm test > test_results.txt
          # Simulate some test outcomes for demonstration
          echo "::set-output name=summary::"
          echo "::set-output name=summary::## Test Report"
          echo "::set-output name=summary::"
          echo "::set-output name=summary::**Status**: \`Passed\`"
          echo "::set-output name=summary::"
          echo "::set-output name=summary::**Details**: See attached \`test_results.txt\` for full output."
          echo "::set-output name=summary::"
          echo "::set-output name=summary::" # Add an empty line for spacing

      - name: Upload test results artifact
        uses: actions/upload-artifact@v4
        with:
          name: test-report
          path: test_results.txt
          retention-days: 1

      - name: Build application
        run: npm run build

      - name: Announce build completion
        run: echo "Build complete!"

    # This is where the magic happens for custom summaries
    container:
      image: node:20
      # The 'output' directive is key here
      output: |
        name: Build and Test Summary
        steps:
          - name: Test Summary

            content: ${{ steps.run_tests.outputs.summary }}

          - name: Build Status
            content: |
              Application build was successful.
              Artifacts are available for download.

In this example, the build_and_test job has a container section. Within container, the output directive is used to define a custom summary. The steps array within output allows you to specify different sections of your summary. Each step has a name and content. The content can be plain text or Markdown, and crucially, it can reference outputs from previous steps using the steps.<step_id>.outputs.<output_name> syntax.

Here, the Test Summary step pulls its content directly from the summary output of the Run tests step. The Run tests step itself uses echo "::set-output name=summary::..." to write Markdown content to a special output variable named summary. This Markdown is then embedded into the job summary. The Build Status step provides a static message.

The problem this solves is that the default GitHub Actions job summaries are often just a list of steps executed. While useful for debugging, they don’t always highlight key outcomes like test pass/fail rates, code coverage, or deployment status. Custom summaries allow you to curate the most important information, making it immediately visible in the workflow run’s "Summary" tab. This is invaluable for quickly assessing the health of a code change without diving into individual job logs.

Internally, GitHub Actions processes these ::set-output commands and the container.output directive. The ::set-output commands are interpreted by the runner to set specific output variables for a step. The container.output directive tells the runner to assemble a custom summary page using the provided structure and content, pulling in step outputs where specified. You have precise control over the structure and content of this summary. You can create multiple named sections, include rich Markdown, and reference any output variable set by previous steps in the same job.

The most surprising thing about custom job summaries is that you can dynamically generate Markdown content within a run step and then have that content seamlessly integrated into a structured summary defined at the job level. It feels like two separate mechanisms, but they are designed to work together, allowing for both dynamic content generation and structured presentation.

The next concept to explore is how to use these custom summaries to integrate with external reporting tools.

Want structured learning?

Take the full Github-actions course →