03 · Cloud Workflows & Orchestration¶
Pub/Sub (Level 2) decouples services with events, but some processes need explicit, ordered steps with branching, retries, and error handling — "call API A, then B with A's result, and if B fails, call C instead." Cloud Workflows is GCP's serverless orchestrator for exactly that: a YAML/JSON-defined sequence of steps that can call HTTP endpoints, GCP APIs, and other workflows, with built-in retry and error-handling constructs.
Defining a workflow¶
# order-workflow.yaml
main:
params: [input]
steps:
- init:
assign:
- order_id: ${input.order_id}
- reserve_inventory:
call: http.post
args:
url: https://inventory-svc-abc.a.run.app/reserve
body:
order_id: ${order_id}
result: reserve_result
- check_reservation:
switch:
- condition: ${reserve_result.body.status == "ok"}
next: charge_payment
next: release_and_fail
- charge_payment:
call: http.post
args:
url: https://payments-svc-abc.a.run.app/charge
body:
order_id: ${order_id}
result: payment_result
next: return_success
- release_and_fail:
call: http.post
args:
url: https://inventory-svc-abc.a.run.app/release
body:
order_id: ${order_id}
next: fail
- return_success:
return: ${payment_result.body}
- fail:
raise: "inventory reservation failed"
gcloud workflows deploy order-workflow \
--source=order-workflow.yaml \
--location=us-central1
gcloud workflows execute order-workflow \
--location=us-central1 \
--data='{"order_id": "ORD-1001"}'
gcloud workflows executions describe EXECUTION_ID \
--workflow=order-workflow --location=us-central1
# state: SUCCEEDED
# result: '{"charge_id":"ch_9f3...","status":"charged"}'
Retries and error handling¶
- reserve_inventory:
try:
call: http.post
args:
url: https://inventory-svc-abc.a.run.app/reserve
body:
order_id: ${order_id}
result: reserve_result
retry:
predicate: ${http.default_retry_predicate}
max_retries: 3
backoff:
initial_delay: 1
max_delay: 10
multiplier: 2
except:
as: e
steps:
- log_failure:
call: sys.log
args:
text: ${"reserve failed: " + e.message}
severity: ERROR
- fail_step:
next: release_and_fail
http.default_retry_predicate retries on 429, 5xx, and connection
errors — not on 4xx client errors, which is usually what you want since
retrying a malformed request just wastes time and money identically each
attempt.
Gotcha — steps execute with a per-workflow execution deadline. By
default a workflow execution can run up to one year for standard workflows,
but individual call: http.* steps carry their own default timeout
(shorter). Long-polling patterns need an explicit timeout argument on the
call, or the step fails well before the outer workflow would.
Gotcha — switch needs an explicit fallthrough. If no condition
matches and no next is set outside the switch block, the workflow throws
a runtime error rather than silently continuing — always give switch a
default next (as check_reservation does above with release_and_fail)
or an explicit unmatched-case branch.
Workflows vs. Cloud Composer vs. Eventarc¶
| Cloud Workflows | Cloud Composer (Airflow) | Eventarc | |
|---|---|---|---|
| Model | Explicit step sequence, sync/async HTTP calls | DAG of scheduled/dependent tasks | Reactive: event in → trigger fires |
| Best for | Request/response orchestration across services | Data pipeline scheduling, backfills | Wiring GCP service events to a handler |
| Infra to manage | None (serverless) | A managed Airflow environment (persistent, billed) | None (serverless) |
| Typical trigger | API call, Pub/Sub message, Scheduler | Cron / sensor-based | GCS object created, Pub/Sub, Cloud Audit Log events |
Workflows is the right tool when you need explicit control flow and can tolerate calling out to HTTP endpoints; Composer is right for data-pipeline DAGs with many interdependent scheduled tasks; Eventarc (Level 4) is right for "when X happens in GCP, run Y" without hand-rolled polling.
IAM for calling other services¶
The workflow's own service account needs permission to invoke whatever it calls — Cloud Run services, other workflows, etc.
gcloud run services add-iam-policy-binding inventory-svc \
--region=us-central1 \
--member="serviceAccount:workflow-sa@my-project.iam.gserviceaccount.com" \
--role="roles/run.invoker"
gcloud workflows deploy order-workflow \
--source=order-workflow.yaml \
--location=us-central1 \
--service-account=workflow-sa@my-project.iam.gserviceaccount.com
Gotcha — default compute service account lacks run.invoker by
default. If you don't specify --service-account at deploy time,
Workflows uses the project's default compute SA, which typically cannot
invoke a private Cloud Run service — calls fail with 403 even though the
workflow itself deployed fine. Always create and scope a dedicated
workflow SA.
Cheat sheet¶
| Command | Purpose |
|---|---|
gcloud workflows deploy |
Create/update a workflow from a YAML/JSON source file. |
gcloud workflows execute |
Run a workflow with input data. |
gcloud workflows executions describe |
Check status/result of a specific execution. |
try / retry / except |
Built-in per-step retry and error-handling. |
http.default_retry_predicate |
Standard retry-on-5xx/429 policy. |
roles/run.invoker on the workflow's SA |
Required to call a private Cloud Run service. |
How It Actually Works¶
Cloud Workflows executes a YAML/JSON-defined state machine where each
step is checkpointed to durable storage before the next one runs — this
is what makes workflow execution resilient to the underlying compute
being recycled mid-run: if the execution engine's worker restarts, it
resumes from the last checkpointed step rather than replaying the whole
workflow from scratch, and (unlike a plain script) a long-running wait
or external callback step can pause execution for hours or days without
holding any compute resource at all — the workflow's state simply sits
serialized in storage until the triggering event or timer fires. This
checkpoint-based execution is also why workflow steps calling external
APIs need explicit retry policies configured rather than relying on
try/catch semantics from a general-purpose language: a step that fails
after the call already had a side effect (e.g., a message already
published) will re-execute that step on retry unless you've made the
downstream call idempotent, the same at-least-once problem that shows up
in Pub/Sub.
Exercise¶
Write a workflow with three HTTP steps calling three different Cloud Run
services (they can be stub services that just echo their input), where step
2 uses try/retry/except and step 3 only runs on success via switch.
Deploy it with a dedicated service account holding roles/run.invoker on
all three services, execute it, and inspect gcloud workflows executions
describe to confirm the retry step's behavior when you deliberately break
one service's URL.