Playbooks

Automation Playbooks

Use these production-ready patterns to automate scans, Kubernetes/container checks, configuration, reports, schedules, webhooks, and MCP tool flows with the current API v1 contract.

These playbooks assume HTTPS local API with self-signed TLS and bearer-token authentication.

Docs > API & Automation > API Playbooks

Use this page after the API Reference when you need runnable automation patterns and operational runbooks.

Preflight: Runtime and Auth Baseline

Set base URL and token once, then verify runtime status and auth gate.

export CWS_BASE_URL="https://192.168.1.4:9098"
export CWS_TOKEN="YOUR_API_TOKEN"

# Public route
curl -k "$CWS_BASE_URL/status"

# Authenticated route
curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/meta/capabilities"

Playbook 1: Normalize Notification Policy State

Set a valid notification trigger mode before automation patches. Accepted values are scan_complete and waste_only.

# Read current policy
curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/notifications/policy"

# Normalize to a valid mode
curl -k -X PATCH "$CWS_BASE_URL/v1/notifications/policy" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"notification_trigger_mode":"scan_complete"}'

Playbook 2: Account Selection and Connectivity Test

List account IDs for targeting and run credential tests before starting scan jobs.

curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/accounts"

curl -k -X POST "$CWS_BASE_URL/v1/cloud-accounts/test" -H "Authorization: Bearer $CWS_TOKEN" -H "Content-Type: application/json" -d '{
    "provider": "unsupported-provider",
    "credentials": "{\"foo\":\"bar\"}"
  }'

Note: unsupported-provider currently validates format only and can return ok=true with an implementation message.

Playbook 3: Trigger Scan and Poll to Completion

Create async scan job, poll status, and stop when terminal state is reached.

Full scan: call POST /v1/scans with no body (or {}). The examples below show a targeted run with selected_accounts.

curl -k -X POST "$CWS_BASE_URL/v1/scans" -H "Authorization: Bearer $CWS_TOKEN"
scan_id=$(curl -k -X POST "$CWS_BASE_URL/v1/scans" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"selected_accounts":["profile_abc123"],"report_emails":["service@cloud-waste-scanner.com"]}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["scan_id"])')

echo "scan_id=$scan_id"

while true; do
  status=$(curl -k -H "Authorization: Bearer $CWS_TOKEN" \
    "$CWS_BASE_URL/v1/scans/$scan_id" \
    | python3 -c 'import sys,json; print(json.load(sys.stdin).get("status",""))')
  echo "status=$status"
  if [ "$status" = "completed" ] || [ "$status" = "failed" ] || [ "$status" = "canceled" ]; then
    break
  fi
  sleep 2
done

curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/scans/$scan_id"

Playbook 4: Generate and Download Report Artifacts

Generate report artifacts through API and download by report_id.

report_id=$(curl -k -X POST "$CWS_BASE_URL/v1/reports/generate" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"format":"pdf","include_esg":true}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["report_id"])')

echo "report_id=$report_id"

curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/reports/$report_id"

curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/reports/$report_id/download" -o "report_${report_id}.pdf"

Playbook 5: Recurring Schedule plus Run-Now

Create persistent schedule payload once and trigger immediate runs when needed.

schedule_id=$(curl -k -X POST "$CWS_BASE_URL/v1/schedules" -H "Authorization: Bearer $CWS_TOKEN" -H "Content-Type: application/json" -d '{
    "name":"nightly-cost-scan",
    "enabled":true,
    "run_at":1762473600,
    "interval_minutes":1440,
    "timezone":"UTC",
    "scan":{"selected_accounts":["profile_abc123"],"report_emails":["service@cloud-waste-scanner.com"]}
  }' | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')

echo "schedule_id=$schedule_id"

curl -k -X POST "$CWS_BASE_URL/v1/schedules/$schedule_id/run-now" -H "Authorization: Bearer $CWS_TOKEN"

Playbook 6: Webhook and MCP Integration Smoke Test

Validate event push path and MCP scan tool path in one flow.

# Create webhook (replace URL with your receiver)
curl -k -X POST "$CWS_BASE_URL/v1/webhooks" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/cws-webhook","events":["scan.completed"],"is_active":true,"secret":"shared-secret"}'

# MCP run scan
mcp_scan_id=$(curl -k -X POST "$CWS_BASE_URL/v1/mcp/tools/run-scan" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"demo_mode":true}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["scan_id"])')

curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/mcp/tools/get-scan/$mcp_scan_id"

Playbook 7: Create, Update, and Delete a Cloud Account

Run full account lifecycle through API. This endpoint family is for non-AWS providers; AWS is managed through local AWS profile flow by design.

# Create
account_id=$(curl -k -X POST "$CWS_BASE_URL/v1/cloud-accounts" -H "Authorization: Bearer $CWS_TOKEN" -H "Content-Type: application/json" -d '{
    "provider":"alibaba",
    "name":"prod-cn",
    "credentials":"{\"access_key_id\":\"ALI_ACCESS_KEY_ID\",\"access_key_secret\":\"ALI_ACCESS_KEY_SECRET\"}",
    "timeout_seconds":12
  }' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')

echo "account_id=$account_id"

# Read
curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/cloud-accounts/$account_id"

# Update
curl -k -X PATCH "$CWS_BASE_URL/v1/cloud-accounts/$account_id" -H "Authorization: Bearer $CWS_TOKEN" -H "Content-Type: application/json" -d '{
    "name":"prod-cn-finops",
    "timeout_seconds":20
  }'

# Delete
curl -k -X DELETE "$CWS_BASE_URL/v1/cloud-accounts/$account_id" -H "Authorization: Bearer $CWS_TOKEN"

Playbook 8: Import Cloud Accounts from JSON and Run Scan

Batch-import account definitions from JSON, capture returned account IDs, and run a scan with only the imported scope.

# 1) Prepare import file
cat > accounts.json <<'JSON'
[
  {
    "provider": "alibaba",
    "name": "import-cn-prod",
    "credentials": {
      "access_key_id": "ALI_ACCESS_KEY_ID",
      "access_key_secret": "ALI_ACCESS_KEY_SECRET"
    },
    "timeout_seconds": 12
  },
  {
    "provider": "azure",
    "name": "import-eu-prod",
    "credentials": {
      "tenant_id": "AZ_TENANT_ID",
      "client_id": "AZ_CLIENT_ID",
      "client_secret": "AZ_CLIENT_SECRET",
      "subscription_id": "AZ_SUBSCRIPTION_ID"
    }
  }
]
JSON

# 2) Import each item via /v1/cloud-accounts and collect IDs
> imported_ids.txt
jq -c '.[]' accounts.json | while IFS= read -r item; do
  payload=$(printf '%s' "$item" | jq -c '
    {
      provider,
      name,
      credentials: (.credentials | tostring),
      timeout_seconds,
      policy_custom,
      proxy_profile_id
    } | with_entries(select(.value != null))
  ')
  account_id=$(curl -k -X POST "$CWS_BASE_URL/v1/cloud-accounts" \
    -H "Authorization: Bearer $CWS_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$payload" \
    | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
  echo "$account_id" >> imported_ids.txt
done

# 3) Convert imported IDs to selected_accounts JSON array
selected_accounts=$(python3 - <<'PY'
import json
with open("imported_ids.txt", "r", encoding="utf-8") as f:
    ids = [line.strip() for line in f if line.strip()]
print(json.dumps(ids))
PY
)

# 4) Start scan for imported accounts only
scan_id=$(curl -k -X POST "$CWS_BASE_URL/v1/scans" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"selected_accounts\":$selected_accounts}" \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["scan_id"])')

echo "scan_id=$scan_id"

# 5) Poll final state
curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/scans/$scan_id"

Playbook 9: Weekly Governance Loop with Full-Scope Fallback

Use requested account targets when they exist, auto-fallback to full-scope scan when they do not, and persist a weekly schedule with the validated scan scope.

This playbook explicitly covers edge handling for POST /v1/scans: no body (full scan), empty target resolution, and targeted-run fallback.

export CWS_BASE_URL="https://192.168.1.4:9098"
export CWS_TOKEN="YOUR_API_TOKEN"
export CWS_TARGET_IDS="profile_aws_prod,profile_ali_cn"   # optional; empty means full-scope scan
export CWS_REPORT_EMAIL="finops@company.com"               # optional

# 1) Normalize runtime settings and notification policy
curl -k -X PATCH "$CWS_BASE_URL/v1/settings/general" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"currency":"USD","api_timeout_seconds":15}'

curl -k -X PATCH "$CWS_BASE_URL/v1/notifications/policy" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"notification_trigger_mode":"scan_complete"}'

# 2) Resolve requested targets against current account inventory
accounts_json=$(curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/accounts")

selected_accounts=$(python3 - <<'PY' <<<"$accounts_json"
import json
import os
import sys

accounts = json.loads(sys.stdin.read() or "[]")
wanted = [item.strip() for item in os.getenv("CWS_TARGET_IDS", "").split(",") if item.strip()]
available = {item.get("id", "") for item in accounts if item.get("id")}
selected = [item for item in wanted if item in available]
print(json.dumps(selected))
PY
)

export SELECTED_ACCOUNTS_JSON="$selected_accounts"

poll_scan_until_terminal() {
  local scan_id="$1"
  while true; do
detail=$(curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/scans/$scan_id")
    status=$(python3 -c 'import sys,json; print(json.load(sys.stdin).get("status",""))' <<<"$detail")
    echo "scan_id=$scan_id status=$status"
    if [ "$status" = "completed" ] || [ "$status" = "failed" ] || [ "$status" = "canceled" ]; then
      printf "%s" "$detail" > "scan_${scan_id}.json"
      break
    fi
    sleep 2
  done
}

# 3) Start scan with adaptive scope
if [ "$selected_accounts" = "[]" ]; then
  echo "No valid target IDs matched. Starting full-scope scan (no body)."
  scan_scope="full"
scan_id=$(curl -k -X POST "$CWS_BASE_URL/v1/scans" -H "Authorization: Bearer $CWS_TOKEN" | python3 -c 'import sys,json; print(json.load(sys.stdin)["scan_id"])')
else
  scan_scope="targeted"
  scan_payload=$(python3 - <<'PY'
import json
import os

payload = {"selected_accounts": json.loads(os.environ["SELECTED_ACCOUNTS_JSON"])}
email = os.getenv("CWS_REPORT_EMAIL", "").strip()
if email:
    payload["report_emails"] = [email]
print(json.dumps(payload))
PY
)
  scan_id=$(curl -k -X POST "$CWS_BASE_URL/v1/scans" \
    -H "Authorization: Bearer $CWS_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$scan_payload" \
    | python3 -c 'import sys,json; print(json.load(sys.stdin)["scan_id"])')
fi

poll_scan_until_terminal "$scan_id"

status=$(python3 -c 'import sys,json; print(json.load(sys.stdin).get("status",""))' < "scan_${scan_id}.json")
if [ "$status" = "failed" ] && [ "$scan_scope" = "targeted" ]; then
  echo "Targeted scan failed. Retrying once with full-scope scan."
  scan_scope="full_fallback"
scan_id=$(curl -k -X POST "$CWS_BASE_URL/v1/scans" -H "Authorization: Bearer $CWS_TOKEN" | python3 -c 'import sys,json; print(json.load(sys.stdin)["scan_id"])')
  poll_scan_until_terminal "$scan_id"
fi

# 4) Persist weekly schedule using validated scope and run once now
schedule_payload=$(python3 - <<'PY'
import json
import os
import time

selected = json.loads(os.environ["SELECTED_ACCOUNTS_JSON"])
email = os.getenv("CWS_REPORT_EMAIL", "").strip()
scan = {}
if selected:
    scan["selected_accounts"] = selected
if email:
    scan["report_emails"] = [email]
payload = {
    "name": "weekly-governance-loop",
    "enabled": True,
    "run_at": int(time.time()) + 300,
    "interval_minutes": 10080,
    "timezone": "UTC",
    "scan": scan
}
print(json.dumps(payload))
PY
)

schedule_id=$(curl -k -X POST "$CWS_BASE_URL/v1/schedules" \
  -H "Authorization: Bearer $CWS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$schedule_payload" \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')

curl -k -X POST "$CWS_BASE_URL/v1/schedules/$schedule_id/run-now" -H "Authorization: Bearer $CWS_TOKEN"

echo "schedule_id=$schedule_id"
  • Edge case covered: if requested IDs are missing or stale, the flow automatically switches to full-scope scan by omitting request body.
  • Edge case covered: if targeted scan fails, the flow retries once with full-scope before persisting schedule.
  • Contract note: schedule payload keeps scan: {} for full-scope mode, or selected_accounts for stable targeted mode.

Playbook 10: Kubernetes and Container Waste Scan

Use the Local API to run a read-only Kubernetes scan through local kubectl, then fetch findings, inventory, and governance reports without sending kubeconfig or scan results to a hosted control plane.

  • Prerequisite: kubectl is installed on the operator machine.
  • Prerequisite: kubeconfig/context has read-only access to pods, nodes, PVs, PVCs, services, deployments, StatefulSets, and DaemonSets.
  • Scope note: this is local kubectl execution, not a direct kube-apiserver SDK client.
export KUBECONFIG_PATH="/home/operator/.kube/config"
export KUBE_CONTEXT="prod"

kubectl --kubeconfig "$KUBECONFIG_PATH" --context "$KUBE_CONTEXT" version --client

curl -k -H "Authorization: Bearer $CWS_TOKEN"   "$CWS_BASE_URL/v1/k8s/contexts?path=$KUBECONFIG_PATH"

curl -k -X POST "$CWS_BASE_URL/v1/k8s/scans"   -H "Authorization: Bearer $CWS_TOKEN"   -H "Content-Type: application/json"   -d "{\"kubeconfig_path\":\"$KUBECONFIG_PATH\",\"kube_context\":\"$KUBE_CONTEXT\"}"

curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/k8s/findings"
curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/reports/k8s-governance"
curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/reports/k8s-action-plan"

If you want a combined cloud-plus-Kubernetes run, call POST /v1/scans with include_kubernetes=true, kubeconfig_path, and kube_context.

Playbook 11: OpenAPI, SDK, Pagination, and Webhook Verification

Use this flow when you are wiring CWS into internal automation and need a stable contract, paginated reads, and signed downstream events.

# Discover the active route contract.
curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/openapi.json"

# Check versioning, pagination, and webhook-signing policy.
curl -k -H "Authorization: Bearer $CWS_TOKEN" "$CWS_BASE_URL/v1/meta/compatibility"

# Read findings with cursor pagination envelope.
curl -k -H "Authorization: Bearer $CWS_TOKEN"   "$CWS_BASE_URL/v1/findings?envelope=true&limit=50"

Failure Handling Runbook

  • If request is rejected immediately, verify HTTPS/base URL, bearer token, and payload schema first.
  • If settings patch fails with 400, check notification_trigger_mode value is scan_complete or waste_only.
  • If scan enters failed, inspect job error plus route/proxy diagnostics and rerun with smaller selected_accounts scope.
  • If report delivery fails, keep scan evidence and retry report generation/download as an independent step.
  • If webhook delivery fails, run webhook test endpoint and compare destination reachability and response status.

Map failure classes to stable codes in API Errors Catalog, then confirm endpoint-specific recovery behavior in API Reference.

Quota Safety Guarantees

  • If no active checks run for selected accounts, the scan is rejected and not counted toward scan quota.
  • If all checks fail and no cloud data is collected, the scan is rejected and not counted toward scan quota.
  • Connection/proxy/notification test operations do not consume scan quota.
Automation Playbooks

Run the playbooks against your own local API endpoint.

Save your first $1,000 before the next billing cycle.