Helm’s define and call directives are how you create reusable chunks of Go template logic.

Let’s say you’re templating a Kubernetes Deployment and you have a common pattern for defining environment variables that you want to reuse across multiple resources or even different charts.

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:

  name: {{ .Release.Name }}-myapp

spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: nginx:latest
        ports:
        - containerPort: 80
        env:

        {{- template "mychart.environment" . }}

Now, you define that reusable logic in a separate file, typically in a _helpers.tpl file within your templates/ directory.

# templates/_helpers.tpl


{{/* Define a named template */}}


{{- define "mychart.environment" -}}

- name: MY_ENV_VAR_1
  value: "some_value_1"
- name: MY_ENV_VAR_2
  value: "some_value_2"

{{- end -}}

When Helm renders this chart, it first processes all the define blocks, registering those named templates. Then, when it encounters {{- template "mychart.environment" . }} in deployment.yaml, it looks up the template named "mychart.environment" and executes its content, passing the current scope (.) to it. The output of this execution is then inserted directly into the deployment.yaml file.

The key here is that define declares a template, and call (or template as we used above, which is a form of calling) executes it. You can pass arguments to templates using the with or range keywords within the called template, or by passing a different scope directly. For instance, if your _helpers.tpl had a template that expected a specific map:

# templates/_helpers.tpl (continued)


{{- define "mychart.configMapEnv" -}}


{{- with .Values.appConfig -}}


{{- range $key, $value := . -}}


- name: {{ $key | upper | quote }}


  value: {{ $value | quote }}


{{- end -}}


{{- end -}}


{{- end -}}

And you would call it like this in your deployment.yaml:

# templates/deployment.yaml (continued)
        env:

        {{- template "mychart.configMapEnv" . }}

This would look for appConfig in your values.yaml and iterate over its key-value pairs to create environment variables.

The real power comes when you use include. While template inserts the content of the template, include inserts the result of executing the template, allowing you to pipe the output of a called template into another function.

Consider a scenario where you want to generate a common label for all your Kubernetes resources.

# templates/_helpers.tpl (continued)


{{- define "mychart.labels" -}}


app.kubernetes.io/name: {{ .Chart.Name | quote }}


app.kubernetes.io/instance: {{ .Release.Name | quote }}


app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}


helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }}


{{- end -}}

You could then use include to apply these labels to a Service:

# templates/service.yaml
apiVersion: v1
kind: Service
metadata:

  name: {{ .Release.Name }}-myapp-svc

  labels:

  {{- include "mychart.labels" . | nindent 4 }}

spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

Here, {{- include "mychart.labels" . | nindent 4 }} first calls the mychart.labels template, gets its output (the label definitions), and then pipes that output to nindent 4, which indents each line of the output by 4 spaces. This is crucial for correctly formatting YAML. Without include, template would just insert the raw label lines, likely breaking the YAML structure.

The distinction between template and include is subtle but critical: template is for inserting raw text output from a template, while include is for processing that output further, often for formatting or transformation.

Helm’s templating engine is, at its core, Go’s text/template package, but with a rich set of built-in functions and objects specific to Kubernetes and Helm. The define and call (or template) directives are the fundamental building blocks for organizing this logic.

A common pitfall is forgetting to pass the correct scope (.) when calling a template. If a template expects values from .Values.someKey but is called with an empty scope, it will fail. Always ensure you pass the appropriate context.

The next step in mastering Helm templating is understanding how to conditionally include or modify template blocks using if, else, and eq functions.

Want structured learning?

Take the full Helm course →