In a Helm chart, what is the difference between `{{ include "x" . }}` and `{{ template "x" . }}`?
answer
- Only one of the two produces a value
- A pipeline needs something to consume
- Action in the grammar versus function in the map
- One buffers the render and returns a string
basics
~20 sinclude is a function Helm adds: it renders a named template and returns the output as a string, so it can be piped into other functions. template is a Go template action that writes straight to the output and yields no value.
solid answer
~50 sBoth call a named template declared with `define`, and both take exactly one data argument that becomes dot inside it. The difference is what they are. `{{ template "x" . }}` is an **action** in the template language, like `if` or `range`: the engine executes the named template and writes those bytes into the output stream at that point. It is not an expression, so it has no value to pipe. Helm adds `include` to the function map: it renders the named template into a buffer and **returns a string**, which means the result can be piped, assigned to a variable, or passed to another function. Because chart output is indentation-sensitive YAML, almost every partial has to be re-indented at the call site, and that needs a value — so idiomatic charts use `include` everywhere and reserve `template` for the rare case where the output already lands at the right place.
code
yaml · 12 lines{{- define "fraud-scoring.commonLabels" -}}
app.kubernetes.io/name: fraud-scoring
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-fraud-scoring
labels:
{{- include "fraud-scoring.commonLabels" . | nindent 4 }}
annotations:
checksum/labels: {{ include "fraud-scoring.commonLabels" . | sha256sum | quote }}go deeper
Recall the rule and the reason in one breath: include returns a string, template writes directly, so anything you need to indent, hash or quote must go through include.
Be ready to explain the mechanics — an action belongs to the language grammar and yields no value, while include is a function in Helm's function map that renders into a buffer. Explain why YAML indentation makes this decisive.
Show that you know the failure is silent: piping a template action rebinds its data argument instead of transforming its output, so the manifest renders subtly wrong rather than failing. Say how you would catch that in review or in a render diff.
Own the convention: a chart standard that says 'always include' removes a whole class of silent defects for every author who touches the chart, and it costs nothing. Be able to argue why you enforce it in review rather than leaving it to taste.
## What a named template is A named template is a reusable fragment of a chart declared with `{{- define "fraud-scoring.commonLabels" -}} ... {{- end -}}`. The `define` block itself emits nothing where it sits; it registers the fragment under a name so manifests can call it. Any chart that repeats a label block, a selector, or a probe stanza across several files ends up with a handful of them. ## One is an action, the other is a function The Go template language that renders charts offers exactly one built-in way to call a named template: the `template` action. `{{ template "fraud-scoring.commonLabels" . }}` belongs to the statement grammar of the language, alongside `if`, `range`, `with` and `define` itself. When the engine reaches it, it executes the named template and writes the resulting bytes directly into the output stream at that position. An action is not an expression: it produces no value, so there is nothing for a pipe to consume, nothing to assign to a variable, and nothing to hand to another function. Helm supplies its own function map to the engine, and `include` is the entry that matters most. It takes a template name and a data value, executes that template into an internal buffer, and returns the buffer's contents as a string. Being an ordinary function call, it is a pipeline expression: `{{ include "fraud-scoring.commonLabels" . | quote }}` and `{{- $labels := include "fraud-scoring.commonLabels" . }}` both work. ## Why that difference decides how charts are written Charts emit YAML, and YAML cares where a block starts. A partial that emits two label lines writes them at column zero inside its `define`, but the call site needs them at four spaces under `metadata.labels` and at six under a pod template. Re-indenting rendered text means feeding it through an indentation function, which requires a value, which only `include` produces. That single fact is why the chart scaffolding Helm generates calls `include` everywhere, and why "use `include`, never `template`" is the standard chart-authoring rule. The same property buys other things. A checksum annotation that rolls the pods of a fraud-scoring endpoint whenever a rendered ConfigMap changes is just an `include` piped through a hashing function. `include ... | fromYaml` lets one partial post-process another's output as data. `include ... | trim | empty` lets a manifest decide whether a partial produced anything before emitting the parent key at all — impossible with an action, whose output has already been written by the time you could test it. ## The trap: piping an action does not fail loudly The grammar of the action is `{{ template "name" pipeline }}`, where the pipeline is the **data argument**. So `{{ template "x" . | quote }}` is perfectly legal and does something you did not ask for: it calls the partial with `quote .` as its data. Dot inside the partial is now a quoted string rather than the render context, so `.Release.Name` and `.Values...` resolve to nothing and the block renders empty or malformed. Nothing errors. Authors usually meet this difference as a subtly wrong manifest rather than as a failed render, which is exactly why interviewers ask about it. ## The calling convention is identical Neither form takes more than one data argument. `include "fraud-scoring.commonLabels" .` hands the partial the caller's whole context; `include "fraud-scoring.commonLabels" .Values.scorer` hands it only that subtree, and inside the partial `.Release` and everything else is then out of reach. If a partial needs two independent inputs, the caller must bundle them into a single value. ## Practical notes `include` also protects against a partial that ends up calling itself: Helm tracks which templates are currently rendering and aborts with an error naming the template rather than recursing until the process dies. Errors raised inside a partial — for example by `required` — surface as an error on the calling template, with the chart and file named in the message. Is `template` ever the right choice? Only where the rendered text belongs at exactly the position it is written and nothing needs to touch it: a whole document, or a paragraph in `NOTES.txt`. It is not wrong there, merely not pipeable. Most teams still standardise on `include` so that no reviewer has to work out which case a given call is.
- Is there any case where writing `template` in a chart is still reasonable?Yes, where the rendered text already belongs exactly where it is written and nothing needs to transform it — a whole document emitted by one partial, or a paragraph in `NOTES.txt`. It is not incorrect there, just not pipeable. Most teams still standardise on `include` so reviewers never have to judge which case a call is, and so that adding an indent or a checksum later is a one-word change.
- How would you make a manifest emit a parent key only when a partial actually produces output?Capture the partial's output first, because you can test a string but not an action: `{{- $extra := include "fraud-scoring.extraLabels" . }}` and then `{{- if not (empty (trim $extra)) }}` before writing the key and the value. That pattern is how charts avoid emitting an empty `labels:` or `annotations:` mapping, which YAML would turn into a null value that the API server rejects.
- What does the second argument to `include` control?It is the value bound to dot inside the named template — the only input the partial gets. Passing `.` gives it the caller's whole context, so `.Values`, `.Release` and `.Chart` all resolve. Passing a subtree such as `.Values.scorer` gives it only that data, and everything else becomes unreachable inside the partial. There is no second data argument; a partial needing two inputs must be handed one bundled value.
template is a rubber stamp pressed straight onto the page wherever your hand happens to be; include hands you the wet ink, so you can indent it, hash it, or decide not to use it at all.
saying these in an interview costs you the question
- Says include and template are interchangeable
- Thinks piping a template action indents its output
- Believes a define block renders where it is written
- Claims include accepts several data arguments
- Thinks template is the Helm addition and include is built in