Tekton Pipelines can pass results between tasks, but the mechanism is a bit more implicit than you might expect, relying on shared storage and environment variables rather than explicit "return values."
Let’s see this in action. Imagine a simple pipeline with two tasks: the first task generates a value, and the second task consumes it.
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: pass-results-pipeline
spec:
tasks:
- name: generate-value
taskSpec:
steps:
- name: generate
image: ubuntu
script: |
echo "my_secret_value_123" > $(results.my-output.path)
results:
- name: my-output
description: The generated value
type: string
- name: consume-value
taskSpec:
params:
- name: input-value
type: string
steps:
- name: consume
image: ubuntu
script: |
echo "The consumed value is: $(params.input-value)"
runAfter:
- generate-value
# This is where the magic happens: linking results to params
finally:
- name: link-results-to-params
taskSpec:
steps:
- name: link
image: alpine
script: |
echo "Linking $(tasks.generate-value.results.my-output) to $(tasks.consume-value.params.input-value)"
This example shows a conceptual link, but Tekton doesn’t have a finally block to directly re-assign task results to other task parameters within the Pipeline definition itself. The standard way to achieve this is by declaring the result in the upstream task and then referencing it as a parameter in the downstream task.
Here’s how it actually works with a correct Pipeline definition:
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: pass-results-pipeline
spec:
tasks:
- name: generate-value
taskRef: # Or taskSpec if defined inline
name: generate-task # Assuming generate-task is a Task resource
results:
- name: my-output
from:
- $(tasks.generate-value.results.my-output) # This syntax is incorrect for linking within the Pipeline spec itself.
- name: consume-value
taskRef: # Or taskSpec if defined inline
name: consume-task # Assuming consume-task is a Task resource
params:
- name: input-value
value: $(tasks.generate-value.results.my-output) # THIS is how you pass results!
runAfter:
- generate-value
Let’s break down the mental model. A Tekton Task can declare results. When a Task runs as part of a Pipeline, it writes its declared results to a specific location on the persistent volume shared by the Pipeline Run. This location is determined by Tekton and is typically within a directory like /tekton/results/. The actual filename corresponds to the result’s name.
So, in our generate-value task, the script echo "my_secret_value_123" > $(results.my-output.path) writes the string "my_secret_value_123" into a file named my-output within the /tekton/results/ directory.
Now, for the consume-value task to use this, it needs to declare a param that expects this value. In the Pipeline definition, we then link the result from the generate-value task to the param of the consume-value task using the value field: value: $(tasks.generate-value.results.my-output).
When the consume-value task starts, Tekton intercepts this parameter substitution. It reads the content of the /tekton/results/my-output file (which was written by the generate-value task) and injects it as the value for the input-value parameter. The consume step in the consume-value task then sees $(params.input-value) resolved to "my_secret_value_123".
The key insight here is that results are not "returned" in a programmatic sense. They are files written to a shared filesystem, and parameters are substituted with the contents of those files. This makes the system robust and allows for passing complex data structures if needed (though strings are the most common).
The $(results.my-output.path) syntax is used within a Task’s steps to know where to write the result. The $(tasks.<task-name>.results.<result-name>) syntax is used within a Pipeline definition to reference that written result and pass it to another task’s parameter.
The most surprising thing about this mechanism is how it abstracts away the underlying storage. You don’t explicitly manage volumes for passing results; Tekton handles the mounting and path management for you, making it appear as if results are directly "passed."
If you were to define a Task that produces a result but then forget to declare that result in the TaskSpec of the upstream Task, the Pipeline would fail with an error indicating an unknown result.