Skip to main content

Multi-Step Tasks

Multi-step tasks enable you to create complex workflows by combining multiple commands, tasks, and scripts into a single, orchestrated operation. They provide advanced control flow, error handling, and parallel execution capabilities.

Basic Multi-Step Structure

tasks:
deploy:
description: Deploy application
steps:
- command: git pull origin main
- command: npm install
- command: npm run build
- command: pm2 restart app

Step Types

Command Steps

Execute shell commands:

tasks:
setup:
steps:
- command: apt-get update
- command: apt-get install -y nginx
- command: systemctl start nginx

Task Steps

Call other tasks:

tasks:
backup-db:
command: pg_dump production > backup.sql

backup-files:
command: tar -czf files.tar.gz /var/www

full-backup:
steps:
- task: backup-db
- task: backup-files
- command: rsync -av /backup/ remote:/backup/

Script Steps

Execute JavaScript code:

tasks:
process:
steps:
- script: |
const data = await fetchData();
console.log(`Processing ${data.length} items`);
return data;
register: fetched_data

- script: |
const processed = fetched_data.map(transform);
await saveResults(processed);

Step Properties

Named Steps

Add names for clarity:

tasks:
deploy:
steps:
- name: Pull latest code
command: git pull origin main

- name: Install dependencies
command: npm install

- name: Run tests
command: npm test

- name: Build application
command: npm run build

- name: Deploy to production
command: ./deploy.sh

Step Targets

Override task target per step:

tasks:
multi-server:
steps:
- name: Update web server
command: apt-get update
target: hosts.web-server

- name: Update database server
command: apt-get update
target: hosts.db-server

- name: Update cache server
command: apt-get update
target: hosts.cache-server

Step Environment

Set environment per step:

tasks:
build-multiple:
steps:
- name: Build for development
command: npm run build
env:
NODE_ENV: development

- name: Build for production
command: npm run build
env:
NODE_ENV: production
MINIFY: true

Parallel Execution

Parallel Steps

Execute steps concurrently:

tasks:
parallel-deploy:
steps:
- name: Deploy services
parallel: true
steps:
- command: deploy-web.sh
target: hosts.web
- command: deploy-api.sh
target: hosts.api
- command: deploy-worker.sh
target: hosts.worker

Parallel with Groups

Group parallel operations:

tasks:
complex-parallel:
steps:
- name: Prepare all servers
parallel: true
steps:
- command: prepare.sh
targets: ["hosts.web-1", "hosts.web-2"]

- name: Deploy application
parallel: true
steps:
- command: deploy.sh
targets: ["hosts.web-1", "hosts.web-2"]

- name: Verify deployment
command: health-check.sh
targets: ["hosts.web-1", "hosts.web-2"]

Conditional Execution

When Conditions

Execute steps conditionally:

A registered variable holds the step's trimmed output as a string and is referenced in conditions as ${vars.<name>}:

tasks:
smart-deploy:
steps:
- name: Check environment
command: echo $ENVIRONMENT
register: env_check

- name: Deploy to staging
command: deploy-staging.sh
when: ${vars.env_check} == "staging"

- name: Deploy to production
command: deploy-production.sh
when: ${vars.env_check} == "production"

Complex Conditions

Conditions support ==, !=, <, <=, >, >=, &&, ||, !, and parentheses. References use ${...} syntax; method calls are not supported:

tasks:
conditional:
steps:
- command: check-health
register: health

- command: restart-service
when: ${vars.health} != "healthy"

- command: scale-up
when: ${vars.health} == "high-load" && ${params.autoScale} == true

Error Handling

Step Failure Behavior

tasks:
resilient:
steps:
- name: Critical step
command: important-operation
onFailure: abort # Stop task (default)

- name: Optional step
command: nice-to-have
onFailure: continue # Continue to next step

- name: Ignorable step
command: cleanup-maybe
onFailure: ignore # Don't count as failure

Retry Logic

tasks:
with-retry:
steps:
- name: Connect to service
command: test-connection
onFailure:
retry: 3
delay: 5s

The retry handler supports retry (number of attempts) and delay (fixed pause between attempts, e.g. 5s). There is no backoff option — the delay is constant across attempts. An onFailure handler can also run a recovery task: or command: instead of retrying.

Error Recovery

tasks:
with-recovery:
steps:
- name: Main operation
command: risky-operation
onFailure:
task: recovery-task

- name: Alternative approach
command: backup-operation
onFailure:
command: emergency-cleanup

Data Flow

Register Variables

Pass data between steps:

The registered value is the step's trimmed stdout (a string), for both command: and script: steps:

tasks:
data-pipeline:
steps:
- name: Get version
command: cat version.txt
register: version

- name: Build with version
command: docker build -t app:${vars.version} .

- name: Tag latest
command: docker tag app:${vars.version} app:latest
when: ${vars.version} == "stable"

To pass structured data between steps, print it as JSON in one step and parse it in the next (e.g. with jq), or do the whole flow inside a single script: step.

Step Ordering and Task Dependencies

Steps always run in the order they are written (unless parallel: true groups them). There is no per-step dependsOn; to run prerequisite tasks first, reference them as steps:

tasks:
prepare-db:
command: setup-database.sh

prepare-cache:
command: setup-cache.sh

deploy-app:
steps:
- task: prepare-db # Run another task as a step
- task: prepare-cache
- command: deploy.sh

(The schema also accepts a task-level dependsOn: list, but it is only validated — the executor does not run dependencies automatically.)

Always Run Steps

Execute regardless of previous failures:

tasks:
with-cleanup:
steps:
- name: Main operation
command: process-data

- name: Risky operation
command: dangerous-task

- name: Cleanup
command: cleanup-resources
alwaysRun: true # Runs even if previous steps failed

- name: Send notification
command: notify-complete
alwaysRun: true

Hooks

Task-Level Hooks

tasks:
monitored:
hooks:
before:
- command: echo "Task starting at $(date)"
- command: check-prerequisites

after:
- command: echo "Task completed at $(date)"
- command: cleanup-temp

onError:
- command: send-alert
- command: rollback

steps:
- command: main-operation
- command: verify-results

Hooks exist at the task level only — individual steps do not accept a hooks: key. To bracket a critical step with setup/verification, make them explicit neighboring steps (with alwaysRun: true for cleanup) or use the step's onFailure handler.

Real-World Examples

CI/CD Pipeline

tasks:
ci-pipeline:
description: Complete CI/CD pipeline
steps:
- name: Checkout code
command: git checkout ${params.branch}

- name: Install dependencies
command: npm ci
timeout: 300000

- name: Run linting
command: npm run lint
onFailure: continue

- name: Run tests
command: npm test
onFailure: abort

- name: Build application
command: npm run build
env:
NODE_ENV: production

- name: Build Docker image
command: |
docker build -t app:${params.version} .
docker tag app:${params.version} app:latest

- name: Push to registry
command: docker push app:${params.version}
when: ${params.branch} == "main"

- name: Deploy to staging
task: deploy-staging
when: ${params.branch} == "develop"

- name: Deploy to production
task: deploy-production
when: ${params.branch} == "main" && ${params.deploy} == true

Database Migration

tasks:
db-migration:
description: Safe database migration with rollback
steps:
- name: Create backup
command: |
f=backup-$(date +%Y%m%d).sql
pg_dump production > "$f"
echo "$f"
target: hosts.db-primary
register: backup_file

- name: Verify backup
command: test -s ${vars.backup_file}
onFailure: abort

- name: Run migration
command: psql production < migration.sql
target: hosts.db-primary
onFailure:
command: psql production < ${vars.backup_file}

- name: Update replicas
parallel: true
steps:
- command: pg_dump production | psql replica
target: hosts.db-replica-1
- command: pg_dump production | psql replica
target: hosts.db-replica-2

- name: Cleanup old backup
command: find /backup -name "*.sql" -mtime +30 -delete
alwaysRun: true

Blue-Green Deployment

tasks:
blue-green-deploy:
description: Zero-downtime blue-green deployment
steps:
- name: Identify current environment
command: kubectl get service app -o jsonpath='{.spec.selector.version}'
register: current_env

- name: Set target environment
command: |
[ "${vars.current_env}" = "blue" ] && echo green || echo blue
register: deploy_env

- name: Deploy to inactive environment
command: |
kubectl set image deployment/app-${vars.deploy_env} \
app=myapp:${params.version}

- name: Wait for rollout
command: |
kubectl rollout status deployment/app-${vars.deploy_env} \
--timeout=600s
onFailure:
retry: 3
delay: 30s

- name: Run smoke tests
command: ./smoke-test.sh ${vars.deploy_env}
onFailure:
command: kubectl rollout undo deployment/app-${vars.deploy_env}

- name: Switch traffic
command: |
kubectl patch service app \
-p '{"spec":{"selector":{"version":"${vars.deploy_env}"}}}'

- name: Verify switch
command: curl -f https://app.example.com/health
onFailure:
retry: 5
delay: 10s

- name: Scale down old environment
command: kubectl scale deployment/app-${vars.current_env} --replicas=0
when: ${params.keepOld} != true

Best Practices

1. Use Named Steps

# Good - clear step purposes
steps:
- name: Install dependencies
command: npm install
- name: Run tests
command: npm test

# Bad - unclear steps
steps:
- command: npm install
- command: npm test

2. Handle Errors Appropriately

steps:
- name: Critical operation
command: important-task
onFailure: abort

- name: Optional enhancement
command: nice-to-have
onFailure: continue

3. Use Parallel Execution Wisely

# Good - independent operations
steps:
- name: Parallel independent tasks
parallel: true
steps:
- command: backup-db
- command: backup-files
- command: backup-configs

# Bad - dependent operations
steps:
- parallel: true
steps:
- command: stop-service
- command: start-service # Depends on stop!

4. Clean Up Resources

steps:
- name: Main operation
command: create-resources

- name: Cleanup
command: delete-resources
alwaysRun: true

5. Document Complex Logic

tasks:
complex-workflow:
description: |
This task performs a rolling update with health checks.
It will automatically rollback on failure.
Expected duration: 15-20 minutes
steps:
# Step descriptions...

Next Steps

See Also