<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.
Basic usage
Section titled “Basic usage”<Command id="trigger-deploy" command="curl -X POST https://api.example.com/deploy" title="Trigger Deployment" successMessage="Deployment triggered!" failMessage="Failed to trigger deployment"/>vs. Check
Section titled “vs. Check”Command and Check blocks share most of their features. A Check reads the state of the world and validates it. A Command changes it.
Required props
Section titled “Required props”id(string): unique identifier for this command block
Optional props
Section titled “Optional props”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 orpath.path(string): path to a shell script file relative to the runbook. Use either this orcommand.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_TOKENandGITHUB_USER, orGITLAB_TOKENandGITLAB_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. PrefergitAuthId.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 to3600000(60 minutes). Raise it for scripts that run longer, for exampletimeoutMs={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 tofalse. While it runs, no other Command or Check block can start, and it can’t start while another one is running. See Run order.
Inline content
Section titled “Inline content”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.
Writing scripts
Section titled “Writing scripts”Scripts can be defined inline with the command prop or stored in external files referenced by the path prop.
Defining scripts
Section titled “Defining scripts”Inline scripts
Section titled “Inline scripts”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.
External scripts
Section titled “External scripts”<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 0else log_error "Deployment failed" exit 1fiIf 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 codes
Section titled “Exit codes”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.
Logging
Section titled “Logging”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.
Log levels
Section titled “Log levels”| 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.
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)Usage example
Section titled “Usage example”#!/bin/bashlog_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 1fi
log_info "Deployment complete"Log files
Section titled “Log files”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:
# tofu's errors appear as [ERROR] lines; its JSON output stays in the variablePLAN_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.
Local development
Section titled “Local development”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):
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.shThis 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.
With variables
Section titled “With variables”Using inputsId
Section titled “Using inputsId”The command or script pulls its values from a separate Inputs block.
<Inputs id="repo-config">```yamlvariables: - 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"/>Using inline inputs
Section titled “Using inline inputs”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>Using multiple inputsIds
Section titled “Using multiple inputsIds”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">```yamlvariables: - 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.
How values are inserted
Section titled “How values are inserted”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}}`}}'.
Execution context
Section titled “Execution context”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.
Run order
Section titled “Run order”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 untilbuildhas produced that output, and can’t run whilebuildis 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
exclusiveblock. No other Command or Check block can start while anexclusiveblock runs, and anexclusiveblock 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.
Background processes
Section titled “Background processes”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:
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.
Examples
Section titled “Examples”Simple deployment script
Section titled “Simple deployment script”#!/bin/bashset -e # Exit on error
log_info "Starting deployment..."kubectl apply -f deployment.yamlkubectl rollout status deployment/myapp
log_info "Deployment complete!"Parameterized script
Section titled “Parameterized script”#!/bin/bashREGION="{{ .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!"Script with error handling
Section titled “Script with error handling”#!/bin/bashset -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!"Capturing output files
Section titled “Capturing output files”Scripts can write files to two destinations:
See the Files workspace page for more.
Writing to generated files
Section titled “Writing to generated files”Files written to $GENERATED_FILES appear in the file panel after the script exits 0 or 2.
Basic example
Section titled “Basic example”#!/bin/bash# Export OpenTofu outputs to generated filestofu output -json > "$GENERATED_FILES/tf-outputs.json"
# Copy a config filecp 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!"/>Organizing files with subdirectories
Section titled “Organizing files with subdirectories”#!/bin/bashmkdir -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
How it works
Section titled “How it works”- Before your script runs, Runbooks creates a temporary capture directory
- The
$GENERATED_FILESenvironment variable points to this directory - Your script writes files to
$GENERATED_FILES - If the script exits 0 or 2, the files are copied to the generated files directory
- The temporary directory is removed
Writing to repository files
Section titled “Writing to repository files”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 repositoryecho "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.
Viewing logs
Section titled “Viewing logs”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.
Block outputs
Section titled “Block outputs”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/bashACCOUNT_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.