Skip to content
Author with AI

<Command>

The <Command> block runs a shell command or script, with user input substituted in. Use it for deployments, resource creation, and anything else that changes state.

<Command
id="trigger-deploy"
command="curl -X POST https://api.example.com/deploy"
title="Trigger Deployment"
successMessage="Deployment triggered!"
failMessage="Failed to trigger deployment"
/>

Command and Check blocks share most of their features. A Check reads the state of the world and validates it. A Command changes it.

  • id (string): unique identifier for this command block
  • title (string): display title shown in the UI. Supports inline markdown (bold, italic, links, code).
  • description (string): longer description of what the command does. Supports inline markdown.
  • command (string): inline command to execute. Use either this or path.
  • path (string): path to a shell script file relative to the runbook. Use either this or command.
  • inputsId (string | string[]): ID of an Inputs block to get template variables from. Can be a single ID or an array of IDs. When multiple IDs are provided, variables are merged in order, and later IDs override earlier ones.
  • awsAuthId (string): ID of an AwsAuth block for AWS credentials. The credentials are passed as environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION). The Run button is disabled until authentication completes.
  • gitAuthId (string): ID of a GitAuth, GitHubAuth or GitLabAuth block, for either provider. Its credentials are passed as environment variables (GITHUB_TOKEN and GITHUB_USER, or GITLAB_TOKEN and GITLAB_USER). The Run button is disabled until authentication completes.
  • githubAuthId (string): ID of a GitHubAuth block. GitHub only. The credentials are passed as environment variables (GITHUB_TOKEN, GITHUB_USER). The Run button is disabled until authentication completes. Prefer gitAuthId.
  • googleAuthId (string): ID of a GoogleAuth block for Google Cloud credentials. The block’s credentials are passed as environment variables (GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_CLOUD_PROJECT, CLOUDSDK_CORE_PROJECT, GOOGLE_PROJECT, CLOUDSDK_CORE_ACCOUNT, and the region/zone variables), overriding the session environment for this execution only. The Run button is disabled until authentication completes.
  • successMessage (string): message shown when the command succeeds. Default “Success”. Supports inline markdown.
  • failMessage (string): message shown when the command fails. Default “Failed”. Supports inline markdown.
  • runningMessage (string): message shown while running. Default “Running…”. Supports inline markdown.
  • timeoutMs (number): maximum time, in milliseconds, the script may run before it is killed and the block is marked as failed. Defaults to 3600000 (60 minutes). Raise it for scripts that run longer, for example timeoutMs={7200000} for a 2-hour deployment, or lower it so a hung script fails sooner.
  • dependsOn (string | string[]): ID of a Command or Check block, or an array of IDs, that must finish with a success or a warning before this block can run. See Run order.
  • exclusive (boolean): whether this block runs alone. Defaults to false. While it runs, no other Command or Check block can start, and it can’t start while another one is running. See Run order.

You can nest an <Inputs> component directly inside the Command:

<Command
id="echo-message"
command='echo "Hello, {{ .inputs.Name }}!"'
title="Print Greeting"
>
<Inputs id="inline-greeting">
```yaml
variables:
- name: Name
type: string
description: Your name
validations:
- required
```
</Inputs>
</Command>

The embedded <Inputs> renders inside the Command block, so users fill in variables before running the command. Other blocks can reference it by inputsId like any standalone Inputs block.

Scripts can be defined inline with the command prop or stored in external files referenced by the path prop.

For simple commands, define the script directly in the command prop:

<Command
id="deploy-trigger"
command="curl -X POST https://api.example.com/deploy"
title="Trigger Deployment"
successMessage="Deployment triggered!"
failMessage="Failed to trigger deployment"
/>

Inline scripts suit one-liners. For anything longer, use an external script file.

<Command
id="deploy-app"
path="scripts/deploy.sh"
title="Deploy Application"
description="This will deploy your application to production"
successMessage="Deployment successful!"
failMessage="Deployment failed. Check the logs for details."
/>

External scripts are plain bash scripts. The referenced scripts/deploy.sh might look like:

#!/bin/bash
log_info "Starting deployment..."
kubectl apply -f deployment.yaml
if kubectl rollout status deployment/myapp; then
log_info "Deployment complete!"
exit 0
else
log_error "Deployment failed"
exit 1
fi

If you edit the script file while the runbook is open, the block shows a “Script changed” notice with the diff and keeps running the version it loaded. Click Reload script in the notice to run the new version. See Reloading a changed script file.

Exit code 0 renders as success (green). Any other exit code renders as failure (red).

Exit code 2 is a warning for Check blocks. A Command that exits 2 is shown as failed, but Runbooks still treats the run as a warning internally. Files written to $GENERATED_FILES are captured, and the script’s environment changes carry forward.

Runbooks sources a logging.sh file into every bash script, which defines a set of logging functions. Using them lets the Runbooks UI parse log levels for filtering and export.

Function Output Description
log_info "msg" [timestamp] [INFO] msg General informational messages
log_warn "msg" [timestamp] [WARN] msg Warning conditions
log_error "msg" [timestamp] [ERROR] msg Error messages
log_debug "msg" [timestamp] [DEBUG] msg Debug output (only when DEBUG=true)

The logging functions append to a log file that Runbooks creates for each run, and Runbooks shows those lines in the block’s logs as your script writes them, in the order it wrote them. They never write to stdout, so you can call them inside a function whose output you capture with $(...). The captured value holds only what the function prints.

Terminal window
get_account_id() {
log_info "Looking up the AWS account ID..."
aws sts get-caller-identity --query Account --output text
}
ACCOUNT_ID=$(get_account_id)
#!/bin/bash
log_info "Starting deployment..."
log_debug "Target environment: $ENVIRONMENT"
if [ -z "$API_KEY" ]; then
log_warn "API_KEY not set, some features may be unavailable"
fi
if ! deploy_application; then
log_error "Deployment failed"
exit 1
fi
log_info "Deployment complete"

Each run gets a set of log files. Runbooks puts their paths in environment variables, the way $RUNBOOK_OUTPUT holds the path of the outputs file:

Variable Written by Level in the logs
RUNBOOK_LOG log_info, log_warn, log_error, log_debug The level each line names
RUNBOOK_INFO_LOG Your commands INFO
RUNBOOK_WARN_LOG Your commands WARN
RUNBOOK_ERROR_LOG Your commands ERROR
RUNBOOK_DEBUG_LOG Your commands DEBUG

All four logging functions append to $RUNBOOK_LOG, so their lines keep the order your script wrote them in, whatever their level. Runbooks shows the lines in $RUNBOOK_LOG as they are written, so a line you add there yourself should name its level the same way, for example [WARN] Quota is almost used up.

The per-level files are for output that doesn’t name a level, such as a tool’s stderr. Any command can append to them directly. Runbooks adds a timestamp and the file’s level to each line that doesn’t already start with them, so the line shows up at that level:

Terminal window
# tofu's errors appear as [ERROR] lines; its JSON output stays in the variable
PLAN_JSON=$(tofu show -json plan.tfplan 2>>"$RUNBOOK_ERROR_LOG")

Append with >>. Overwriting a log file with > can drop or repeat lines.

Scripts in other languages get the same variables. For example, in Python:

import os
with open(os.environ["RUNBOOK_WARN_LOG"], "a") as log:
print("Quota is almost used up", file=log)

Runbooks reads the log files while your script runs, so their lines appear next to its stdout and stderr, normally in the order the script wrote them. Lines written to different log files close together can appear grouped by file, with the lines from $RUNBOOK_LOG first. log_debug only writes when DEBUG=true, but every line in $RUNBOOK_DEBUG_LOG is shown, whoever wrote it.

The files are deleted when the run ends, so lines that a background job writes after the script exits don’t appear. From then on, the logging functions write to stderr instead, so the job’s later log lines go wherever you redirected its output. Don’t redirect a background job’s output to the log files: once they are deleted, a command whose output is redirected to one of them fails without running.

Outside the Runbooks UI, the logging functions aren’t injected. To simulate the Runbooks environment, run this one-liner in your terminal session (bash or zsh):

Terminal window
mkdir -p ~/.config/runbooks && curl -fsSL https://raw.githubusercontent.com/gruntwork-io/runbooks/main/scripts/logging.sh -o ~/.config/runbooks/logging.sh && export BASH_ENV=~/.config/runbooks/logging.sh

This sets BASH_ENV, which tells bash to source the logging functions before running any script. It works from bash and zsh parent shells and lasts until the terminal is closed. The same script then works in both environments.

Outside Runbooks RUNBOOK_LOG isn’t set, so the logging functions write to stderr instead. The one-liner overwrites ~/.config/runbooks/logging.sh, so re-run it to pick up changes to the logging functions. Copies downloaded before the log files existed write to stdout, where the log lines end up in $(...) captures.

The command or script pulls its values from a separate Inputs block.

<Inputs id="repo-config">
```yaml
variables:
- name: OrgName
type: string
description: GitHub organization name
- name: RepoName
type: string
description: Repository name
```
</Inputs>
<Command
id="create-repo"
command='gh repo create "{{ .inputs.OrgName }}/{{ .inputs.RepoName }}" --private'
inputsId="repo-config"
title="Create GitHub Repository"
successMessage="Repository {{ .inputs.RepoName }} created!"
failMessage="Failed to create repository"
/>

The Command collects input values directly. Other blocks can read them like any standalone Inputs block.

<Command
id="echo-message"
command='echo "Hello, {{ .inputs.Name }}!"'
title="Print Greeting"
>
<Inputs id="inline-greeting">
```yaml
variables:
- name: Name
type: string
description: Your name
validations:
- required
```
</Inputs>
</Command>

Pass an array of IDs to reference several Inputs blocks. Variables are merged in order, and later IDs override earlier ones:

<Inputs id="lambda-config" templatePath="templates/lambda" />
<Inputs id="repo-config">
```yaml
variables:
- name: GithubOrgName
type: string
description: GitHub organization name
- name: GithubRepoName
type: string
description: Repository name
```
</Inputs>
<Command
id="deploy-lambda"
path="scripts/deploy.sh"
inputsId={["lambda-config", "repo-config"]}
title="Deploy Lambda Function"
description="Deploy the Lambda function using variables from both inputs"
/>

If both blocks define a variable with the same name, the value from repo-config wins.

Variable values are inserted into the script verbatim, with no quoting or escaping. Quote a reference where the shell needs it, for example REGION="{{ .inputs.AwsRegion }}", and compare strings directly, for example {{ if eq .inputs.Language "JavaScript" }}.

If the script can’t be rendered, for example because it calls an unknown function or contains a {{ }} expression meant for another tool, the block fails with the template error and the script does not run. To pass {{ }} through to a tool such as docker ps --format, write it as a raw string: --format '{{`{{.Names}}`}}'.

Scripts run in a persistent environment. Environment variable changes (export, unset) and working directory changes (cd) made by a script that exits 0 or 2 carry forward to later blocks, so earlier steps can set up the environment for later ones.

Scripts run in a non-interactive shell, so shell aliases such as ll and shell functions such as nvm or rvm are not available.

See Shell execution context for the details.

Command and Check blocks can run at the same time. A block’s Run button is disabled while the block has to wait for another one:

  • It uses another block’s outputs. A block that references {{ .outputs.build.tag }} can’t run until build has produced that output, and can’t run while build is running again.
  • It names another block in dependsOn. Use this when a script needs something an earlier script did that isn’t an output, such as a variable it exported, a directory it changed into, or a resource it created. The block can’t run until every block it names has finished with a success or a warning. If one of them later fails or is stopped, the block waits again.
  • It would overlap with an exclusive block. No other Command or Check block can start while an exclusive block runs, and an exclusive block can’t start while another one is running. Use this for a script that can’t share its resources, such as one that takes a state lock.
<Command id="login" command="export API_TOKEN=$(vault read -field=token secret/api)" />
<Command id="deploy" dependsOn="login" command="./deploy.sh" />
<Command id="migrate" dependsOn={["login", "deploy"]} exclusive command="./migrate.sh" />

A block that is waiting names the blocks it is waiting for. Click a name to scroll to that block. For a block that is running, there is also a button to stop it.

A block finishes once its script has exited and no process it started still has the script’s output open. To leave a process running for later blocks, such as a port-forward or a local dev server, start it in the background with its output redirected:

Terminal window
nohup kubectl port-forward svc/web 8080:80 > /tmp/port-forward.log 2>&1 &

Without the redirect, the block keeps waiting on the background process until timeoutMs kills it. Stopping a running block, or letting it hit timeoutMs, terminates the script together with every process it started. Quitting Runbooks while a block is running sends them the same termination signal.

scripts/deploy.sh
#!/bin/bash
set -e # Exit on error
log_info "Starting deployment..."
kubectl apply -f deployment.yaml
kubectl rollout status deployment/myapp
log_info "Deployment complete!"
scripts/create-vpc.sh
#!/bin/bash
REGION="{{ .inputs.AwsRegion }}"
VPC_NAME="{{ .inputs.VpcName }}"
CIDR_BLOCK="{{ .inputs.CidrBlock }}"
log_info "Creating VPC $VPC_NAME in $REGION..."
log_debug "CIDR block: $CIDR_BLOCK"
aws ec2 create-vpc \
--cidr-block "$CIDR_BLOCK" \
--tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=$VPC_NAME}]" \
--region "$REGION"
log_info "VPC created successfully!"
scripts/safe-deploy.sh
#!/bin/bash
set -e
function cleanup {
log_info "Cleaning up..."
# Cleanup code here
}
trap cleanup EXIT
log_info "Running pre-deployment checks..."
./check-prerequisites.sh || { log_error "Pre-checks failed"; exit 1; }
log_info "Deploying..."
./deploy.sh
log_info "Running post-deployment validation..."
./validate-deployment.sh || { log_error "Validation failed"; exit 1; }
log_info "Deployment successful!"

Scripts can write files to two destinations:

See the Files workspace page for more.

Files written to $GENERATED_FILES appear in the file panel after the script exits 0 or 2.

#!/bin/bash
# Export OpenTofu outputs to generated files
tofu output -json > "$GENERATED_FILES/tf-outputs.json"
# Copy a config file
cp config.yaml "$GENERATED_FILES/"

Or as an inline command:

<Command
id="create-file"
title="Create a greeting file"
command={`echo "Hello from the command!" > "$GENERATED_FILES/greeting.txt"`}
successMessage="File created successfully!"
/>
#!/bin/bash
mkdir -p "$GENERATED_FILES/opentofu"
mkdir -p "$GENERATED_FILES/config"
tofu output -json > "$GENERATED_FILES/opentofu/outputs.json"
echo '{"env": "production"}' > "$GENERATED_FILES/config/settings.json"

This creates:

  • Directorygenerated/
    • Directoryopentofu/
      • outputs.json
    • Directoryconfig/
      • settings.json
  1. Before your script runs, Runbooks creates a temporary capture directory
  2. The $GENERATED_FILES environment variable points to this directory
  3. Your script writes files to $GENERATED_FILES
  4. If the script exits 0 or 2, the files are copied to the generated files directory
  5. The temporary directory is removed

Once a <GitClone> block has registered a repository, the $REPO_FILES environment variable points to its local path. With several repositories registered, it points to the one selected in the workspace’s repository dropdown, or to the most recently registered one if the user hasn’t selected any. Scripts can modify files inside the repository directly:

#!/bin/bash
# Modify a file in the cloned repository
echo "new setting = true" >> "$REPO_FILES/config.hcl"

Writes to $REPO_FILES are not captured to a temporary directory. They happen directly on the filesystem, and the Changed files tab shows them via git diff.

While a command runs, its output streams into the collapsible View Logs panel under the block. The panel’s toolbar offers:

  • Copy log file path. Every run also writes its output to a log file on disk, which you can open or tail with other tools.
  • Copy prompt for LLM. Copies a ready-to-paste prompt for an AI assistant. It names the block’s ID, the runbook file and the log file, and ends with an instruction that matches the block’s status. When the block has failed, for example, it asks the assistant to analyze the failure and suggest a fix. The prompt points at the log file rather than including the logs, so it works best with assistants that can read local files, such as Claude Code or Cursor.
  • Wrap long lines, copy the logs as plain text, or download them as raw (.log) or structured (.json) logs.
  • Maximize the logs to fill the screen.

Blocks that don’t write a log file, such as <GitClone> and <GitPullRequest>, put the last 200 lines of their logs in the prompt instead.

Commands can produce outputs that later blocks consume. Scripts write key=value pairs to the $RUNBOOK_OUTPUT file, and later blocks reference them via {{ .outputs.<block-id>.<key> }}.

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

After a block runs, click View Outputs below the log viewer to inspect its outputs. To keep a credential off screen there, write sensitive: before its key, as in echo "sensitive:API_TOKEN=$TOKEN" >> "$RUNBOOK_OUTPUT". See Sensitive outputs.

For producing outputs, consuming them in templates, dependency behavior, and complex data types, see Block outputs in Inputs and outputs.