> For the complete documentation index, see [llms.txt](https://docs.vale.sh/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.vale.sh/topics/templates.md).

# Templates

Learn how to write an output format of your own.

`--output` takes `CLI`, `line`, or `JSON`, and otherwise the name of a template: a [Go template](https://pkg.go.dev/text/template) that Vale renders with the run's alerts in place of a built-in format.

```sh
vale --output=sarif.tmpl docs/ > vale.sarif
```

The value is read as a path first. When no file is there, it is looked up by name on the `StylesPath`, where templates live in `config/templates`.

The exit code is the same as for any other format: `1` when an alert at the `error` level was reported, `0` otherwise, and `--no-exit` turns the first into the second.

## [The data](#the-data)

A template is executed once, with the whole run:

```go
type Data struct {
    Files       []ProcessedFile // every file that had at least one alert
    LintedTotal int             // every file the run read, alerts or not
}

type ProcessedFile struct {
    Path   string
    Alerts []core.Alert
}
```

A file with no alerts is left out of `Files` and counted in `LintedTotal`, which is how a summary line can say "in 12 files" while ranging over three. Each alert has the fields the [JSON output](/topics/cli.md#output) shows: `Check`, `Severity`, `Message`, `Description`, `Link`, `Match`, `Line`, `Span`, `Action`, and `Suggestions`. `Span` is a pair of one-based character columns, and both ends are inclusive.

## [The functions](#the-functions)

Every function in [Sprig](http://masterminds.github.io/sprig/) is available, which covers strings, lists, dictionaries, and arithmetic. Vale adds these:

| Name          | Argument(s)       | Description                                                                                        |
| ------------- | ----------------- | -------------------------------------------------------------------------------------------------- |
| `red`         | `string`          | The string in red.                                                                                 |
| `yellow`      | `string`          | The string in yellow.                                                                              |
| `blue`        | `string`          | The string in blue.                                                                                |
| `underline`   | `string`          | The string underlined.                                                                             |
| `newTable`    | `bool`            | A table with no borders that writes to standard output. The argument says whether cell text wraps. |
| `addRow`      | `Table, []string` | The table with the row appended. Returns the table, so it chains.                                  |
| `renderTable` | `Table`           | Print the table, with a blank line before and after, and empty it for reuse.                       |
| `jsonEscape`  | `string`          | The string escaped for use inside a JSON string literal, without the surrounding quotes.           |

The colors and the underline are terminal escapes, so a template meant to be redirected to a file should leave them out; `--no-color` disables them for a run.

## [Examples](#examples)

### [Customizing the default output](#customizing-the-default-output)

The following example re-implements Vale’s default output style using a template.

```go
{{- /* Keep track of our various counts */ -}}

{{- $e := 0 -}}
{{- $w := 0 -}}
{{- $s := 0 -}}
{{- $f := 0 -}}

{{- /* Range over the linted files */ -}}

{{- range .Files}}
{{$table := newTable true}}

{{- $f = add1 $f -}}
{{- .Path | underline | indent 1 -}}

{{- /* Range over the file's alerts */ -}}

{{- range .Alerts -}}

{{- $error := "" -}}
{{- if eq .Severity "error" -}}
    {{- $error = .Severity | red -}}
    {{- $e = add1 $e  -}}
{{- else if eq .Severity "warning" -}}
    {{- $error = .Severity | yellow -}}
    {{- $w = add1 $w -}}
{{- else -}}
    {{- $error = .Severity | blue -}}
    {{- $s = add1 $s -}}
{{- end}}

{{- $loc := printf "%d:%d" .Line (index .Span 0) -}}
{{- $row := list $loc $error .Message .Check | toStrings -}}

{{- $table = addRow $table $row -}}
{{end -}}

{{- $table = renderTable $table -}}
{{end}}
{{- $e}} {{"errors" | red}}, {{$w}} {{"warnings" | yellow}} and {{$s}} {{"suggestions" | blue}} in {{$f}} {{$f | int | plural "file" "files"}}.
```

### [Creating a RDJSONL template](#creating-a-rdjsonl-template)

The following example converts Vale’s output to [RDJSONL](https://github.com/reviewdog/reviewdog?tab=readme-ov-file#reviewdog-diagnostic-format-rdformat), which you can then pass to [Reviewdog](https://github.com/reviewdog/reviewdog) to display on pull request. This can be useful when the [Vale action](https://github.com/vale-cli/vale-action) is not suitable for your workflow.

```go
{{- /* Range over the linted files */ -}}

{{- range .Files}}

{{- $path := .Path -}}

{{- /* Range over the file's alerts */ -}}

{{- range .Alerts -}}

{{- $error := "" -}}
{{- if eq .Severity "error" -}}
    {{- $error = "ERROR" -}}
{{- else if eq .Severity "warning" -}}
    {{- $error = "WARNING" -}}
{{- else -}}
    {{- $error = "INFO" -}}
{{- end}}

{{- /* Variables setup */ -}}

{{- $line := printf "%d" .Line -}}
{{- $start := index .Span 0 -}}
{{- $end := add (index .Span 1) 1 -}}
{{- $check := printf "%s" .Check -}}
{{- $message := printf "%s" .Message -}}

{{- /* Output */ -}}

{"message": "{{ $message | jsonEscape }}", "location": {"path": "{{ $path }}", "range": {"start": {"line": {{ $line }}, "column": {{ $start }}}, "end": {"line": {{ $line }}, "column": {{ $end }}}}}, "severity": "{{ $error }}", "code": {"value": "{{ $check | jsonEscape }}"{{ if .Link }}, "url": "{{ .Link | jsonEscape }}"{{ end }}}}
{{end -}}
{{end -}}
```

Two things are worth knowing when adapting this. Reviewdog reads a range, so giving it the end of the span -- `Span` is inclusive, and Reviewdog's end is not -- is what underlines the match rather than pointing at its first character. And Reviewdog counts columns in UTF-8 bytes where Vale counts characters, so the two agree only until a line picks up its first multi-byte character; converting between them needs the source line, which a template can't read.

### [Creating a SARIF template](#creating-a-sarif-template)

The following example converts Vale's output to [SARIF](https://sarifweb.azurewebsites.net/), the format that [GitHub code scanning](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github), GitLab, and Azure DevOps read. Unlike a pull request comment, an alert reported this way persists: it has a history, and someone can dismiss it with a reason.

```bash
$ vale --output=sarif.tmpl . > vale.sarif
```

In a GitHub workflow, hand the file to `upload-sarif`:

```yaml
- name: Upload to code scanning
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: vale.sarif
```

Two details make the conversion straightforward. SARIF measures columns in characters, as Vale's `Span` does, so the positions carry over unchanged. And SARIF asks for each rule to be described once, which [`dict`](http://masterminds.github.io/sprig/dicts.html) and `set` collect in a pass over the alerts before any output.

```go
{{- /* Collect the rules that fired, so that each is described once. */ -}}
{{- $rules := dict -}}
{{- range .Files -}}
{{- range .Alerts -}}
{{- $_ := set $rules .Check (dict "link" .Link "text" .Description) -}}
{{- end -}}
{{- end -}}
{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "Vale",
          "informationUri": "https://vale.sh",
          "rules": [
{{- $first := true -}}
{{- range $id, $rule := $rules }}
{{ if not $first }},{{ end }}            {
              "id": "{{ $id | jsonEscape }}",
              "shortDescription": {"text": "{{ if $rule.text }}{{ $rule.text | jsonEscape }}{{ else }}{{ $id | jsonEscape }}{{ end }}"}
              {{- if $rule.link }},
              "helpUri": "{{ $rule.link | jsonEscape }}"
              {{- end }}
            }
{{- $first = false -}}
{{- end }}
          ]
        }
      },
      "results": [
{{- $first = true -}}
{{- range .Files -}}
{{- $path := .Path -}}
{{- range .Alerts }}
{{ if not $first }},{{ end }}        {
          "ruleId": "{{ .Check | jsonEscape }}",
          "level": "{{ if eq .Severity "error" }}error{{ else if eq .Severity "warning" }}warning{{ else }}note{{ end }}",
          "message": {"text": "{{ .Message | jsonEscape }}"},
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {"uri": "{{ $path | jsonEscape }}"},
                "region": {
                  "startLine": {{ .Line }},
                  "startColumn": {{ index .Span 0 }},
                  "endColumn": {{ add (index .Span 1) 1 }},
                  "snippet": {"text": "{{ .Match | jsonEscape }}"}
                }
              }
            }
          ]
        }
{{- $first = false -}}
{{- end -}}
{{- end }}
      ]
    }
  ]
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.vale.sh/topics/templates.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
