Skip to content
Author with AI

Inputs & Outputs

Every runbook has a global context, a shared set of key-value pairs that blocks read and write. The keys are variables.

Values enter the global context in two ways. Input providers such as <Inputs> and <Template> collect values from the user through a form, and consumer blocks pull them in with the inputsId prop. Blocks that run something, such as <Command>, <Check> and <DirPicker>, publish results that later blocks read through the outputs namespace.

Reference a variable in templates, scripts, and block props as {{ .inputs.VarName }}. For example:

<Command
id="create-account"
command='aws organizations create-account --email "{{ .inputs.Email }}" --account-name "{{ .inputs.AccountName }}"'
/>

When this block runs, Runbooks resolves {{ .inputs.Email }} and {{ .inputs.AccountName }} from the global context, producing a command like:

Terminal window
aws organizations create-account --email "alice@example.com" --account-name "my-account"

Runbooks uses Gruntwork Boilerplate as its template engine, so variables are Boilerplate variables:

  • The same {{ .inputs.VarName }} syntax
  • The same types (string, int, float, bool, enum, list, map)
  • The same validation rules (required, email, url, alpha, digit, alphanumeric, countrycode2, semver, regex)
  • The same boilerplate.yml format for defining them
  • The Boilerplate helper functions and pipe modifiers (upper, lower, snakeCase, camelCase, fromJson, conditionals, loops)

The Boilerplate templates page covers the syntax, variable definitions, helper functions, and testing templates locally with the Boilerplate CLI.

Reference inputs as {{ .inputs.VarName }} and outputs as {{ .outputs.blockId.outputName }}. Runbooks also exposes every input at the root of the context, so {{ .VarName }} works in every block and in templates written for the Boilerplate CLI.

An input provider is a block that collects variable values and makes them available to other blocks.

The <Inputs> block renders a form from a boilerplate.yml definition. This inline definition declares one variable, ProjectName:

<Inputs id="config">
```yaml
variables:
- name: ProjectName
type: string
description: Name for your project
```
</Inputs>

The <Template> block generates files from a Boilerplate template directory. It renders a form for the variables in that directory’s boilerplate.yml, so it is an input provider too.

<Template id="infra" path="templates/rds" />

Here the variables come from templates/rds/boilerplate.yml.

Any block that consumes variables (<Command>, <Check>, <Template>, <TemplateInline>) accepts an inputsId prop naming an input provider by its id. This Command uses the value collected by the Inputs block:

<Inputs id="config">
```yaml
variables:
- name: ProjectName
type: string
```
</Inputs>
<Command inputsId="config" command='mkdir "{{ .inputs.ProjectName }}"' />

Pass an array of IDs to inputsId to merge variables from more than one input provider. Variables merge in order, so on a name conflict the later ID wins.

<Command
inputsId={["global-config", "local-config"]}
command='deploy --env "{{ .inputs.Environment }}" --name "{{ .inputs.AppName }}"'
/>

An <Inputs> block nested inside a <Command> or <Check> feeds the parent without inputsId:

<Command command='echo "Hello, {{ .inputs.Name }}!"'>
<Inputs id="greeting">
```yaml
variables:
- name: Name
type: string
```
</Inputs>
</Command>

Other blocks can still reference the embedded Inputs with inputsId="greeting".

A consuming block receives every variable from its input provider.

<Template> also has its own boilerplate.yml, which can define variables too. When a Template imports from an input provider, Runbooks compares the two sets:

  • A variable only the Template defines appears as an editable field in the Template’s form. Use this to collect extra inputs, such as an environment name, that the provider doesn’t know about.
  • A variable only the input provider defines is passed to the template engine and doesn’t appear in the Template’s form.
  • A variable both define appears in the Template’s form read-only, synced to the input provider’s value, so the two forms can’t disagree.

A block that has run can publish outputs for later blocks to read.

<Command> and <Check> blocks produce outputs by writing key=value pairs to the file specified by the $RUNBOOK_OUTPUT environment variable:

#!/bin/bash
ACCOUNT_ID=$(aws organizations create-account ...)
echo "account_id=$ACCOUNT_ID" >> "$RUNBOOK_OUTPUT"
echo "region=us-west-2" >> "$RUNBOOK_OUTPUT"

For complex data (lists, maps), serialize as JSON:

Terminal window
echo 'users=["alice","bob","charlie"]' >> "$RUNBOOK_OUTPUT"

Runbooks sets $RUNBOOK_OUTPUT before the script runs.

When an output holds a credential, such as a secret key, token or password, write sensitive: before its key:

Terminal window
echo "AWS_ACCESS_KEY_ID=$ACCESS_KEY_ID" >> "$RUNBOOK_OUTPUT"
echo "sensitive:AWS_SECRET_ACCESS_KEY=$SECRET_ACCESS_KEY" >> "$RUNBOOK_OUTPUT"
echo "sensitive:AWS_SESSION_TOKEN=$SESSION_TOKEN" >> "$RUNBOOK_OUTPUT"

The prefix isn’t part of the key. The second output above is named AWS_SECRET_ACCESS_KEY, so later blocks read it as {{ .outputs.<block_id>.AWS_SECRET_ACCESS_KEY }}. Auth blocks that take credentials from a block, such as <AwsAuth detectCredentials={[{ block: '<block-id>' }]} />, find it by its usual name too.

Runbooks hands a sensitive output’s real value only to what uses it:

  • A Command or Check script that references it in a template runs with the real value.
  • A <Template>, or a <TemplateInline> with generateFile, writes the real value into the file it generates.
  • An auth block’s detectCredentials block source reads the real value.
  • The copy button on the output’s row in View Outputs copies the real value.
  • The test CLI’s output_equals and output_matches assertions compare the real value.

Everywhere Runbooks only shows the value, it masks it:

  • View Outputs shows ••••••••, and Copy JSON writes "<redacted>".
  • The script view of a Command or Check that references the output shows <redacted> in its place, though the script runs with the real value.
  • A <TemplateInline> without generateFile shows <redacted> in its preview.
  • A block prop that references the output, such as a title, a description, a clone URL or a pull request’s title and body, gets <redacted>.
  • The test CLI prints <redacted> in its --verbose output, in the rendered output of a <TemplateInline> and in the message of a failed output_equals or output_matches assertion.

An empty sensitive output reveals nothing, so wherever <redacted> would stand in for it, it stays empty. An {{ if }} on it then takes the same branch in the script view as when the script runs.

<redacted> only stands in for the value, so a template that processes the value can’t process <redacted>. For example, the script view of {{ (fromJson .outputs.<block_id>.credentials).user }} shows a render error that explains this, though the script runs with the real value. A <TemplateInline> preview of it shows the same error.

Instruction mode treats a sensitive output like one that hasn’t been produced yet, even if its step already ran. The command shows a <name> placeholder, with a field to paste the value into.

Auth blocks mark their own secrets the same way, including a credential they read from a block that marked it sensitive:. <AwsAuth> publishes AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN as sensitive outputs. <GitAuth>, <GitHubAuth> and <GitLabAuth> publish their token as one when their outputs include it, which they do for a personal access token or a { block } source. So {{ .outputs.<auth_block_id>.AWS_SECRET_ACCESS_KEY }} is masked like any other sensitive output.

Once any line marks a key sensitive:, that output stays sensitive, even if a later line writes the same key without the prefix. The prefix is lowercase and comes first on the line.

Later blocks reference outputs through the outputs namespace:

{{ .outputs.<block-id>.<output-name> }}

For example, a Command whose id is create-account that outputs account_id is referenced as:

{{ .outputs.create_account.account_id }}

This syntax works in <Command> and <Check> scripts, <Template> files, and <TemplateInline> content.

Use fromJson to parse JSON output values, then range to iterate:

{{- range (fromJson .outputs.list_users.users) }}
- {{ . }}
{{- end }}

When a block references outputs from a block that hasn’t run yet, a warning names the blocks that need to run first, the Run or Generate button is disabled, and the template shows the raw {{ }} syntax.

A Command or Check block’s button is also disabled while the upstream block is running again. See Run order.

After a block runs, View Outputs below the log viewer lists its outputs in a table. Sensitive outputs are masked there.

Some outputs exist only some of the time. <GitClone> publishes org_id and repo_id only when it can look the repository up on GitHub, and a script can write an output only when it has something to report. Templates treat a missing key as an error, so a block that reads such an output waits until the output exists, which may be never.

To read an output that may be missing, check for it with hasKey and read it only inside that branch:

{{ if hasKey .outputs.clone_repo "org_id" }}
org_id = "{{ .outputs.clone_repo.org_id }}"
{{ end }}

When every reference to an output sits inside such a branch, the block waits for the upstream block to run, but not for that output. The branch is skipped when the output is missing. If the upstream block later withdraws its outputs, for example when a <GitClone> is reset or a <Command> fails, the block waits again.

A guard covers only the branch it opens. A reference anywhere else makes the block wait for the output again, whether it comes after {{ end }}, in an {{ else }} branch, or in another file of the same template. These forms count as a guard:

  • {{ if hasKey .outputs.<block_id> "<output>" }}, or $.outputs in place of .outputs inside range or with
  • {{ if and (hasKey .outputs.<block_id> "<output>") ... }}, which also guards the arguments of and that follow it
  • The {{ else }} branch of {{ if not (hasKey .outputs.<block_id> "<output>") }}

Other forms aren’t recognized, such as a guard on a variable ({{ if hasKey $repo "org_id" }}), so the block waits for the output.

Inputs and block outputs can appear in the same template:

# From an Inputs block (via inputsId)
environment = "{{ .inputs.Environment }}"
# From a Command block's outputs
account_id = "{{ .outputs.create_account.account_id }}"

The block-outputs feature demo passes inputs and outputs between Command, Check, Template, and TemplateInline blocks.

To run it directly:

Terminal window
runbooks open https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/block-outputs