| name | shellflow |
|---|---|
| description | Use when writing or reviewing Shellflow playbooks, especially when mixing |
Shellflow playbooks are normal shell scripts with comment markers. The script remains valid shell for humans, while Shellflow uses markers to split execution into isolated local and remote blocks, pass explicit context between blocks, and emit structured results for agents.
Generate ordinary shell first. Then apply Shellflow rules for block boundaries, prelude freezing, remote host resolution, dynamic options, task selection, lifecycle hooks, and agent-safe output.
Use this skill when:
- Writing a new Shellflow playbook.
- Refactoring a shell script into
# @LOCALand# @REMOTEblocks. - Reviewing Shellflow parser compatibility.
- Explaining block isolation, prelude behavior, exports, tasks, hooks, or parallel execution.
- Preparing a script for
shellflow run --json,--jsonl, oragent-run.
Do not use this skill for:
- Generic bash unrelated to Shellflow.
- Arbitrary SSH orchestration syntaxes that Shellflow does not parse.
- Comma-expanded multi-host syntax inside one
# @REMOTEmarker. - Embedded workflow logic that belongs in the outer agent.
- Use a regular
.shfile. - Keep a shebang when helpful, for example
#!/bin/bash. - Put shell safety flags such as
set -euo pipefailin the prelude only if every block should inherit them. - Prefer clear, self-contained commands over clever shell state.
Top-level markers:
# @LOCAL# @REMOTE <ssh-host># @SERVER <name># @option <name>[=<default>]# @TASK <name># @MACRO <name> .../# @ENDMACRO# @HELPER <name>/# @ENDHELPER# @HOOK <type>/# @ENDHOOK
Block directives must appear immediately after # @LOCAL or # @REMOTE <host>, before command lines:
# @TIMEOUT <seconds># @RETRY <count># @EXPORT NAME=stdout|stderr|output|exit_code# @SHELL bash|zsh|sh# @PARALLEL [group]
Write marker names uppercase even though the parser accepts them case-insensitively.
Good:
# @LOCAL
echo "build locally"
# @REMOTE staging
uname -aBad:
echo "# @LOCAL"
# marker text inside an echo is just shell output, not a block marker# @REMOTE <ssh-host> takes one host alias. Do not write # @REMOTE web-1,web-2; create separate remote blocks and mark them # @PARALLEL if they should run together.
Each block is isolated. Do not rely on these persisting across blocks:
cd- shell variables
- shell functions defined inside a block
- aliases
exportvalues- shell options set inside a block
Use SHELLFLOW_LAST_OUTPUT or # @EXPORT for explicit handoff.
Good:
# @LOCAL
# @EXPORT ARTIFACT=stdout
mktemp
# @LOCAL
test -n "$ARTIFACT"
printf 'ready\n' > "$ARTIFACT"Bad:
# @LOCAL
artifact=$(mktemp)
# @LOCAL
printf 'ready\n' > "$artifact"Lines before the first block marker are the shared prelude. Shellflow prepends non-assignment prelude lines to every executable block.
Uppercase assignments in the prelude are special: Shellflow evaluates them once locally and exports the frozen values into every block.
Good:
#!/bin/bash
set -euo pipefail
BUILD_ID=$(date +%s)
log() {
printf '[deploy] %s\n' "$*"
}
# @LOCAL
echo "$BUILD_ID"
# @REMOTE staging
echo "$BUILD_ID"Both blocks receive the same BUILD_ID.
Avoid one-time side effects in the prelude, especially if uppercase assignments are present and prelude evaluation will run locally:
cd /srv/app # bad in prelude
rm -rf tmp/cache # bad in preludePut one-time operations in explicit blocks instead.
Use # @option to declare parameters a human or agent must fill.
# @option staging
# @option branch=main
# @option release-name=Semantics:
# @option stagingis boolean.--stagingsetsSTAGING=1; absent means the variable is unset.# @option branch=mainhas a default.--branch developsetsBRANCH=develop.# @option release-name=is required. Provide--release-name v1or environment variableRELEASE_NAME.- Option names become uppercase environment variables with dashes converted to underscores.
- Unknown dynamic CLI options are parse errors.
CLI:
shellflow run deploy.sh --branch develop --release-name v2026.05.01 --stagingAgent input:
{
"script": "# @option release-name=\n# @LOCAL\necho \"$RELEASE_NAME\"\n",
"options": {"release-name": "v2026.05.01"},
"dry_run": false
}Use # @TASK <name> to label the following block.
# @TASK build
# @LOCAL
echo "build"
# @TASK deploy
# @REMOTE staging
echo "deploy"Run one task:
shellflow run deploy.sh --task buildUse a single-line macro to define a task flow:
# @MACRO release build deploy smoke-testThen run:
shellflow run deploy.sh --task releaseRules:
- A
--tasktarget can be either a task name or a macro name. - A macro used as a task flow must reference existing task names.
- Unknown task targets must fail parse rather than silently running the whole script.
Macros can also expand command snippets inside blocks:
# @MACRO print_env
# env | sort
# @ENDMACRO
# @LOCAL
print_envHelpers are reusable command snippets. They expand when a block line is exactly the helper name.
# @HELPER backup_db
# pg_dump "$DATABASE_URL" > backup.sql
# @ENDHELPER
# @LOCAL
backup_dbUse helpers for local command reuse. Keep them simple and predictable.
Hooks run locally and share Shellflow context:
PREruns once before main blocks.BEFOREruns before each main block.AFTERruns after each main block.SUCCESSruns after all main blocks succeed.ERRORruns after a hook or main block fails.FINISHEDruns at the end for success or failure.
Aliases:
POSTmeansAFTER.FINALLYmeansFINISHED.
Example:
# @HOOK PRE
# echo "prepare"
# @ENDHOOK
# @HOOK ERROR
# echo "collect diagnostics"
# @ENDHOOK
# @HOOK FINISHED
# echo "cleanup"
# @ENDHOOKUse hooks for setup, cleanup, and diagnostics. Do not hide primary deployment logic in hooks.
# @PARALLEL applies only to the next block, or to the current block when used as a block directive.
Mark every block that should be in the parallel group:
# @PARALLEL web
# @REMOTE web-1
systemctl restart nginx
# @PARALLEL web
# @REMOTE web-2
systemctl restart nginx
# @LOCAL
echo "after the parallel group"Run with:
shellflow run restart.sh --mode parallelWithout --mode parallel, annotated blocks still run sequentially. Consecutive parallel blocks form one group. A non-parallel block ends the group.
Parallel blocks receive a copied context. Do not rely on exports produced by one parallel block being available to another parallel block in the same group.
Remote hosts can come from inline # @SERVER definitions:
# @SERVER staging
# host: 192.168.1.100
# user: deploy
# port: 22
# key: ~/.ssh/id_ed25519
# @REMOTE staging
hostnameOr from SSH config:
Host staging
HostName 192.168.1.100
User deploy
Port 22
IdentityFile ~/.ssh/id_ed25519Rules:
@SERVERrequires ahostfield.- Inline
keymaps to SSH identity file. # @REMOTE <host>must resolve before execution.- Unknown remote hosts fail early.
- Use
--ssh-config <path>to override~/.ssh/config.
Use # @SHELL zsh or # @SHELL bash immediately after a remote block marker when the target needs a specific shell.
# @REMOTE zsh-server
# @SHELL zsh
reload
compdefShellflow starts remote shells in login mode and quietly bootstraps ~/.zshrc or ~/.bashrc for zsh/bash blocks.
Remote command tracing uses a shell DEBUG trap and executes the whole block as one native script. This preserves multi-line shell structures:
# @REMOTE staging
if test -f /srv/app/current; then
echo "exists"
else
echo "missing"
fiDo not split multi-line shell syntax into separate blocks.
Useful commands:
shellflow run script.sh
shellflow run script.sh --json
shellflow run script.sh --jsonl
shellflow run script.sh --no-input
shellflow run script.sh --dry-run
shellflow run script.sh --mode parallel
shellflow run script.sh --task release
shellflow run script.sh --audit-log audit.jsonl --jsonl
shellflow agent-run --json-input '{"script":"# @LOCAL\necho hi\n"}'
shellflow doctor script.shUse:
--jsonfor one final report.--jsonlfor ordered run/block events.--no-inputfor CI and agent runs.--dry-runto inspect the execution plan.--audit-logto write redacted JSONL events.doctor [script]to check install/config status and parse an optional script.
Exit codes:
0: success1: execution failure2: parse failure3: SSH config failure4: timeout failure
Before returning a Shellflow playbook, verify:
- It is valid shell plus supported comment markers.
- Block directives appear immediately after the block marker.
- Every block can run independently in a fresh shell.
- Shared prelude has no accidental one-time side effects.
- Uppercase prelude assignments are intended to be frozen once locally.
- Required
# @option name=values are provided by CLI, env, or agent input. - Cross-block data uses
SHELLFLOW_LAST_OUTPUTor# @EXPORT. - Remote targets resolve through
# @SERVERor SSH config. - Multi-host work uses separate blocks, not comma hosts.
- Each parallel block is explicitly marked and run with
--mode parallel. - Hooks are only for setup, cleanup, success, and failure side effects.
- Agent runs use
--json,--jsonl, oragent-runinstead of scraping human output.
#!/bin/bash
set -euo pipefail
# @option release-name=
# @option branch=main
BUILD_ID=$(date +%Y%m%d%H%M%S)
log() {
printf '[deploy] %s\n' "$*"
}
# @SERVER staging
# host: staging.example.com
# user: deploy
# @HOOK ERROR
# log "deployment failed for $RELEASE_NAME"
# @ENDHOOK
# @HOOK FINISHED
# log "finished $RELEASE_NAME"
# @ENDHOOK
# @MACRO release build deploy smoke
# @TASK build
# @LOCAL
# @EXPORT ARTIFACT=stdout
log "building $RELEASE_NAME from $BRANCH"
printf '/tmp/%s-%s.tar.gz\n' "$RELEASE_NAME" "$BUILD_ID"
# @TASK deploy
# @REMOTE staging
# @TIMEOUT 120
log "deploying $ARTIFACT"
test -n "$ARTIFACT"
# @TASK smoke
# @LOCAL
log "smoke test complete for $RELEASE_NAME"Run it:
shellflow run deploy.sh --task release --release-name v2026.05.01 --branch main --jsonl --no-input- Putting
cd,rm, or deployment commands in the prelude. - Expecting shell variables from one block to exist in the next block.
- Forgetting required dynamic options.
- Using
# @PARALLELonce and expecting it to apply to every later block. - Running parallel annotations without
--mode parallel. - Using
# @REMOTE web-1,web-2instead of separate remote blocks. - Placing
@TIMEOUT,@RETRY,@EXPORT,@SHELL, or@PARALLELafter commands. - Using invalid export sources.
- Treating hook commands as remote cleanup; hooks currently run locally.
- Printing noisy output from a block whose stdout is exported into later blocks.
- Forgetting to quote
"$SHELLFLOW_LAST_OUTPUT"and exported variables.