16. Workflow
App Mesh Workflow provides GitHub Actions-style CI/CD pipelines that run directly on App Mesh. Define your pipeline as a YAML file, register it, and trigger runs via CLI or events.
Every example below is copy-paste runnable against a local App Mesh daemon — commands use echo/sleep/true/false so they work without any extra setup.
16.1. Quick Start
16.1.1. 1. Write a workflow
Save this as hello.yaml:
name: hello-world
jobs:
greet:
steps:
- name: say-hello
command: "echo Hello from App Mesh Workflow!"
16.1.2. 2. Register, run, inspect
# Register
appm workflow add -f hello.yaml
# Trigger a run (note the run_id printed in the message)
appm workflow run hello-world
# List runs
appm workflow runs hello-world
# Trigger a new run and tail its log until terminal
appm workflow run hello-world -f
# Read the run's flow log
appm workflow logs -w hello-world <run_id>
# Read a step's stdout
appm workflow output -w hello-world <run_id> -j greet -s say-hello
# Clean up
appm workflow rm hello-world
16.2. Concepts
A workflow contains one or more jobs. Jobs run in dependency order (DAG). Each job contains sequential steps. Steps execute commands, run existing Apps, send messages, or invoke sub-workflows.
Workflow
├── Job A
│ ├── Step 1 (command)
│ ├── Step 2 (app)
│ └── finally:
│ └── Step cleanup
└── Job B (needs: [A])
├── Step 1 (command)
└── Step 2 (message)
16.3. Step Types
16.3.1. Command Step
Runs a shell command. A temporary App (wf-cmd-*) is created and removed after execution.
Save as command-demo.yaml:
name: command-demo
jobs:
build:
steps:
- name: build
command: "echo build $BUILD_TYPE && sleep 1 && echo done"
workdir: "/tmp"
timeout: 30
env:
BUILD_TYPE: release
appm workflow add -f command-demo.yaml
appm workflow run command-demo -f
appm workflow rm command-demo
The command runs in a shell, so a YAML block scalar (|) gives you a full multi-line script — variables, loops, and conditionals all work:
name: multiline-demo
jobs:
build:
steps:
- name: script
command: |
set -e
VERSION=2.0.1
for stage in lint build test; do
echo "running $stage for $VERSION"
done
if [ -d /tmp ]; then echo "tmp exists"; fi
appm workflow add -f multiline-demo.yaml
appm workflow run multiline-demo -f
appm workflow rm multiline-demo
When passing a step’s output into another command via
${{ steps.x.stdout }}, prefer single-line values — a multi-line stdout substituted into a one-line command will have its later lines executed as separate shell commands. Emit a single line (e.g.echo "$RESULT") for values you intend to interpolate.
16.3.2. App Step
Runs an existing registered App. Useful for long-running services or pre-configured tasks.
# app-step-demo.yaml
name: app-step-demo
jobs:
use-ping:
steps:
- name: ping-once
app: "ping" # an existing registered App
timeout: 10
# Make sure the target App exists first (ping is shipped with the daemon)
appm ls | grep ping
appm workflow add -f app-step-demo.yaml
appm workflow run app-step-demo -f
appm workflow rm app-step-demo
16.3.3. Message Step
Sends a JSON payload to another App via the Task API (run_task). The target App processes it and returns a response.
# message-demo.yaml
name: message-demo
jobs:
call-pytask:
steps:
- name: ask
message:
app: "pytask" # pytask is shipped with the daemon
payload: 'print("hello from message step")'
timeout: 30
appm workflow add -f message-demo.yaml
appm workflow run message-demo -f
appm workflow rm message-demo
16.3.4. Sub-workflow Step
Invokes another registered workflow. Inputs are passed via with, outputs are returned. Nesting is capped at 4 levels.
Save the callee deploy-service.yaml:
name: deploy-service
on:
workflow_call:
inputs:
target:
type: string
required: true
outputs:
deployed_url:
value: "${{ jobs.deploy.steps.publish.stdout }}"
jobs:
deploy:
steps:
- name: publish
command: "echo https://${{ inputs.target }}.example.com"
Save the caller release.yaml:
name: release
jobs:
rollout:
steps:
- name: deploy-staging
workflow: deploy-service
with:
target: staging
timeout: 60
- name: notify
command: "echo deployed to ${{ steps.deploy-staging.outputs.deployed_url }}"
appm workflow add -f deploy-service.yaml
appm workflow add -f release.yaml
appm workflow run release -f
appm workflow rm release
appm workflow rm deploy-service
16.4. Job Dependencies (DAG)
Use needs to define execution order. Jobs without dependencies run in parallel.
# dag-demo.yaml
name: dag-demo
jobs:
build:
steps:
- name: compile
command: "echo building && sleep 1 && echo v1.2.3"
test:
needs: [build]
steps:
- name: run-tests
command: "echo testing ${{ jobs.build.steps.compile.stdout }}"
deploy:
needs: [test]
steps:
- name: deploy
command: "echo deploying"
appm workflow add -f dag-demo.yaml
appm workflow run dag-demo -f
appm workflow detail -w dag-demo <run_id> # see per-job status
appm workflow rm dag-demo
Execution order: build → test → deploy. If build fails, test and deploy are skipped.
16.5. Conditions
Use if on jobs or steps to control execution with expressions.
# conditions-demo.yaml
name: conditions-demo
jobs:
deploy:
steps:
- name: check
command: "echo ready"
- name: deploy
command: "false" # intentionally fails
if: "steps.check.stdout == 'ready'" # step `if` gating on a prior step's output
finally:
- name: rollback
command: "echo rolling back"
if: "steps.deploy.exit_code != 0" # failure handling belongs in finally (see note)
notify:
needs: [deploy]
if: "always()" # runs even though deploy failed
steps:
- name: send-alert
command: "echo deploy status was ${{ jobs.deploy.status }}"
appm workflow add -f conditions-demo.yaml
appm workflow run conditions-demo -f
appm workflow rm conditions-demo
Failure handling must go in
finally. A failed step stops the job (see Error Handling), so a later step in the mainsteps:list — even one withif: failure()orif: "…exit_code != 0"— is never reached. Put rollback/recovery logic infinally(its steps always run and do evaluateif), or setcontinue-on-error: trueon the step that may fail so the following step’sifis evaluated.
Status functions (evaluated against the current job’s steps when used at step/finally level; against dependency jobs when used as a job-level if):
| Function | Meaning |
|---|---|
success() |
All prior steps / dependencies succeeded (default) |
failure() |
At least one prior step / dependency failed |
always() |
Run regardless of status |
16.6. Expressions
Expressions use ${{ }} syntax for variable substitution:
| Pattern | Example | Description |
|---|---|---|
inputs.<key> |
${{ inputs.env }} |
Workflow input value |
steps.<name>.stdout |
${{ steps.build.stdout }} |
Step stdout output (within the same job) |
steps.<name>.exit_code |
${{ steps.build.exit_code }} |
Step exit code |
steps.<name>.outputs.<key> |
${{ steps.deploy.outputs.url }} |
Sub-workflow output (sub-workflow steps only) |
job.status |
${{ job.status }} |
Status of the current job — useful in finally steps |
jobs.<name>.status |
${{ jobs.test.status }} |
Status of another job (success/failure/skipped) |
jobs.<name>.steps.<step>.stdout |
${{ jobs.build.steps.compile.stdout }} |
Cross-job step output |
env.<key> |
${{ env.VERSION }} |
Environment variable |
workflow.name |
${{ workflow.name }} |
Workflow name |
workflow.run_id |
${{ workflow.run_id }} |
Current run ID |
16.7. Error Handling
16.7.1. Default: Stop on Failure
By default, a failed step stops the job. Subsequent steps are skipped. finally steps still run.
# stop-on-fail-demo.yaml
name: stop-on-fail-demo
jobs:
pipeline:
steps:
- name: step-a
command: "false" # exit 1
- name: step-b
command: "echo never runs" # skipped
appm workflow add -f stop-on-fail-demo.yaml
appm workflow run stop-on-fail-demo -f
appm workflow detail -w stop-on-fail-demo <run_id>
appm workflow rm stop-on-fail-demo
16.7.2. Continue on Error
Use continue-on-error: true to proceed after a failure:
# continue-on-error-demo.yaml
name: continue-on-error-demo
jobs:
pipeline:
steps:
- name: lint
command: "false"
continue-on-error: true # failure won't stop the job
- name: test
command: "echo lint failed but I still run"
appm workflow add -f continue-on-error-demo.yaml
appm workflow run continue-on-error-demo -f
appm workflow rm continue-on-error-demo
16.7.3. Retry
Retry a step on failure with fixed or exponential backoff:
# retry-demo.yaml
name: retry-demo
jobs:
flaky:
steps:
- name: deploy
command: "false" # always fails so we see all retries
retry:
max: 3
backoff: exponential # or: fixed
interval: 2 # seconds (base interval)
appm workflow add -f retry-demo.yaml
appm workflow run retry-demo -f
appm workflow rm retry-demo
16.7.4. Finally
finally steps always run after job steps, regardless of success or failure. Use for cleanup.
# finally-demo.yaml
name: finally-demo
jobs:
deploy:
steps:
- name: do-work
command: "false"
finally:
- name: cleanup
command: "echo cleaning tmp files"
- name: report
command: "echo job ended with status ${{ job.status }}"
appm workflow add -f finally-demo.yaml
appm workflow run finally-demo -f
appm workflow rm finally-demo
16.8. Inputs
Define parameters that users provide when triggering a run:
# inputs-demo.yaml
name: inputs-demo
on:
manual:
inputs:
environment:
type: string
required: true
description: "Target environment"
dry_run:
type: string
default: "false"
description: "Dry run mode"
jobs:
deploy:
steps:
- name: run
command: "echo env=${{ inputs.environment }} dry_run=${{ inputs.dry_run }}"
appm workflow add -f inputs-demo.yaml
# Show declared inputs
appm workflow inputs inputs-demo
# Required input must be provided
appm workflow run inputs-demo -e environment=production -e dry_run=true -f
appm workflow rm inputs-demo
Input keys must match
[A-Za-z_][A-Za-z0-9_]*(env-var-safe).
16.9. Concurrency Control
Prevent parallel runs of the same workflow:
# concurrency-demo.yaml
name: concurrency-demo
on:
manual:
inputs:
env:
type: string
default: "staging"
concurrency:
group: "deploy-${{ inputs.env }}"
cancel-in-progress: false # true = cancel existing run instead of queuing
jobs:
slow:
steps:
- name: work
command: "echo working on ${{ inputs.env }} && sleep 5"
appm workflow add -f concurrency-demo.yaml
# First run blocks (5s sleep) — follow it in the background
appm workflow run concurrency-demo -e env=prod -f &
sleep 1 # let the first run claim the group slot
# Second run with same group key prints status=pending and queues
appm workflow run concurrency-demo -e env=prod
wait
# Inspect: the queued run started after the first one completed
appm workflow runs concurrency-demo
appm workflow rm concurrency-demo
Semantics:
Same group key → only one active run at a time
cancel-in-progress: false→ new run queues behind active runcancel-in-progress: true→ active run is cancelled, new run starts
16.10. Remote Execution
Execute jobs on remote App Mesh nodes using label selectors.
By node label (configure labels on each daemon via appm label -a -l role=test-server):
# remote-label-demo.yaml
name: remote-label-demo
jobs:
test:
node_label:
role: "test-server"
steps:
- name: run-tests
command: "echo running on $HOSTNAME"
By explicit host:
# remote-host-demo.yaml
name: remote-host-demo
jobs:
deploy:
node_label:
host: "prod-server-1:6059"
steps:
- name: deploy
command: "echo deploying on $HOSTNAME"
appm workflow add -f remote-label-demo.yaml
appm workflow run remote-label-demo -f
appm workflow rm remote-label-demo
16.11. Triggers
16.11.1. Manual
The default — triggered by appm workflow run.
16.11.2. App Event
Triggered when a registered App emits an event matching the condition.
# trigger-on-event.yaml
name: trigger-on-event
on:
app_event:
app: "data-collector" # an existing App you want to listen to
events: [EXIT]
condition: "exit_code == 0"
jobs:
process:
steps:
- name: handle
command: "echo data-collector finished cleanly"
appm workflow add -f trigger-on-event.yaml
# Whenever data-collector emits EXIT with exit_code 0, this workflow runs.
appm workflow runs trigger-on-event # check accumulated runs
appm workflow rm trigger-on-event
16.11.3. Schedule (External)
Cron scheduling is not built into the workflow engine. Use App Mesh’s native cron support to drive runs:
# Run hello-world every day at 02:00
appm add -a cron-hello -c "appm workflow run hello-world" -Y "0 2 * * *"
# Or, drive on an interval (ISO 8601 duration)
appm add -a tick-hello -c "appm workflow run hello-world" -i PT5M
appm rm -a cron-hello
appm rm -a tick-hello
16.11.4. Workflow Call
Allow the workflow to be invoked as a sub-workflow by another workflow (see Sub-workflow Step):
on:
workflow_call:
inputs:
target:
type: string
required: true
outputs:
deployed_url:
value: "${{ jobs.deploy.steps.publish.stdout }}"
16.12. Encrypted Environment Variables
Use sec_env for sensitive values. They are encrypted at rest by the daemon. env is plaintext; sec_env is encrypted. Both become plain env vars inside the spawned process. sec_env can be set at the workflow, job, or step level (inner level wins on conflict).
# secrets-demo.yaml
name: secrets-demo
env:
API_URL: "https://api.example.com"
sec_env:
API_KEY: "my-secret-token"
jobs:
call-api:
steps:
- name: request
command: 'echo "calling $API_URL with key=${API_KEY:0:4}…"'
appm workflow add -f secrets-demo.yaml
appm workflow run secrets-demo -f
appm workflow rm secrets-demo
16.13. CLI Reference
| Command | Description |
|---|---|
appm workflow add -f <file> |
Register a workflow from YAML (must contain name: and jobs:) |
appm workflow list |
List all registered workflows |
appm workflow get <name> |
Print a workflow's YAML |
appm workflow rm <name> |
Remove a workflow |
appm workflow run <name> [-e key=val] [-f] |
Trigger a run; -f follows output until terminal |
appm workflow runs <name> |
List run history |
appm workflow logs -w <name> <run_id> |
View the run's flow log |
appm workflow output -w <name> <run_id> -j <job> -s <step> |
View a step's stdout |
appm workflow detail -w <name> <run_id> |
Show run detail (per-job status, steps) |
appm workflow cancel -w <name> <run_id> |
Cancel a running workflow |
appm workflow rerun -w <name> <run_id> |
Re-run with the same inputs |
appm workflow inputs <name> |
Show input parameters defined by the workflow |
16.14. Complete Example
A self-contained pipeline that exercises inputs, env, DAG, retry, continue-on-error, finally, and conditions — runnable as-is.
Save as ci-cd.yaml:
name: ci-cd
owner: admin
on:
manual:
inputs:
branch:
type: string
default: "main"
description: "Git branch to build"
environment:
type: string
required: true
description: "Deploy target (staging/production)"
concurrency:
group: "ci-cd-${{ inputs.environment }}"
cancel-in-progress: false
env:
PROJECT: "demo"
jobs:
build:
steps:
- name: checkout
command: "echo checking out branch ${{ inputs.branch }}"
timeout: 30
- name: compile
command: "echo building $PROJECT && sleep 1 && echo v1.2.3"
timeout: 60
retry:
max: 2
backoff: fixed
interval: 2
test:
needs: [build]
steps:
- name: unit-tests
command: "echo unit tests passed"
- name: lint
command: "false" # intentionally fails
continue-on-error: true # but the job continues
deploy:
needs: [test]
if: "success()"
steps:
- name: deploy
command: "echo deploying to ${{ inputs.environment }}"
env:
DEPLOY_ENV: "${{ inputs.environment }}"
- name: health-check
command: "echo health OK"
retry:
max: 3
backoff: exponential
interval: 1
finally:
- name: notify
command: "echo final job status ${{ job.status }}"
# Register
appm workflow add -f ci-cd.yaml
# View declared inputs
appm workflow inputs ci-cd
# Run end-to-end and follow live (note the run_id printed)
appm workflow run ci-cd -e branch=release-v2 -e environment=staging -f
# After completion, inspect details
appm workflow runs ci-cd
appm workflow detail -w ci-cd <run_id>
appm workflow logs -w ci-cd <run_id>
appm workflow output -w ci-cd <run_id> -j deploy -s deploy
# Re-run with same inputs
appm workflow rerun -w ci-cd <run_id>
# Clean up
appm workflow rm ci-cd