# A first look at serverless orchestration with Workflows

Author: [Mete Atamel](https://atamel.dev/) | Published: 2020-09-08 | Updated: 2025-11-21 | Topics: Workflows, Cloud Run, Serverless, How To | Canonical: https://atamel.dev/posts/2020/09-08_first_look_at_workflows/


## Challenges in connecting services

When I think about my recent projects, I probably spent half of my time coding
new services and the other half in connecting services. Service A calling
Service B, or Service C calling an external service and using the result to feed
into another Service D.

Connecting services is one of those things that 'should be easy' but in reality,
it takes a lot of time and effort. You need to figure out a common
connection format for services to use, make the connection, parse the results,
and pass the results on. I'm not even mentioning error handling, retries and
all those production readiness type features that you ultimately need to do.

That's why I got really excited when I saw the following tweet from [Filip
Knapik](https://twitter.com/FilipKnapik1) talking about the beta availability of
a new product called Workflows:

{{< x user="FilipKnapik1" id="1299090679809953796" >}}

## What is Workflows?

In a nutshell, [Workflows](http://cloud.google.com/workflows) allows you to
connect 'things' together. What kind of things? Pretty much anything that has a
public API. You can connect multiple Cloud Functions together, or mix-match
Cloud Functions with Cloud Run services with Google Cloud APIs or even external
APIs.

In addition to connecting disparate services, Workflows handles parsing and
passing values among services. It has built-in error handling and retry
policies. And it's serverless, scales seamlessly with demand with scaling
down to zero.

This sounds great, let's see what we can do with it!

## Connect two Cloud Functions

As a first sample, let's connect two Cloud Functions using Workflows.

First service is
[randomgen](https://github.com/meteatamel/workflow-samples/tree/master/randomgen).
It generates a random number between 1-100:

```python
import random, json
from flask import jsonify

def randomgen(request):
    randomNum = random.randint(1,100)
    output = {"random":randomNum}
    return jsonify(output)
```

Deploy to Cloud Functions:

```sh
gcloud functions deploy randomgen \
    --runtime python37 \
    --trigger-http \
    --allow-unauthenticated
```

Second service is
[multiply](https://github.com/meteatamel/workflow-samples/tree/master/multiply).
It multiplies the received input by 2:

```python
import random, json
from flask import jsonify

def multiply(request):
    request_json = request.get_json()
    output = {"multiplied":2*request_json['input']}
    return jsonify(output)
```

Deploy to Cloud Functions:

```sh
gcloud functions deploy multiply \
    --runtime python37 \
    --trigger-http \
    --allow-unauthenticated
```

Connect the two services using a first workflow. Create a `workflow.yaml` file:

```yaml
- randomgenFunction:
    call: http.get
    args:
        url: https://us-central1-workflows-atamel.cloudfunctions.net/randomgen
    result: randomgenResult
- multiplyFunction:
    call: http.post
    args:
        url: https://us-central1-workflows-atamel.cloudfunctions.net/multiply
        body:
            input: ${randomgenResult.body.random}
    result: multiplyResult
- returnResult:
    return: ${multiplyResult}
```

Notice how easy it is to call different services with just their url, parse the
results of one and feed into the other service.

Deploy the workflow:

```sh
gcloud beta workflows deploy workflow \
    --source=workflow.yaml
```

Execute the workflow:

```sh
gcloud beta workflows execute workflow
```

This gives you a command to see the progress of your workflow. In my case the
command is like this:

```sh
gcloud beta workflows executions describe bc0e0c53-2b77-438a-a2dc-824be577f5f9 \
   --workflow workflow

{
  "body": {
    "multiplied": 34
  },
  "code": 200,
  "headers": {
   ...
```

Notice that we got a `multiplied` value and status code `200`. Nice, our first
workflow is up and running!

## Connect an external service

In the next sample, let's connect to [math.js](https://api.mathjs.org/) as an
external service in the workflow.

In `math.js`, you can evaluate mathematical expressions like this:

```sh
curl https://api.mathjs.org/v4/?expr=log(56)

4.02535169073515
```

This time, we'll use Cloud Console to update our workflow. Find `Workflows` in
Google Cloud Console:

![Workflows menu](/img/2020/workflows-menu.png)

Go to our workflow and click on `Definition` tab:

![Workflows definition](/img/2020/workflows-definition.png)

Edit the definition to include `math.js`:

```yaml
- randomgenFunction:
    call: http.get
    args:
        url: https://us-central1-workflows-atamel.cloudfunctions.net/randomgen
    result: randomgenResult
- multiplyFunction:
    call: http.post
    args:
        url: https://us-central1-workflows-atamel.cloudfunctions.net/multiply
        body:
            input: ${randomgenResult.body.random}
    result: multiplyResult
- logFunction:
    call: http.get
    args:
        url: https://api.mathjs.org/v4/
        query:
            expr: ${"log(" + string(multiplyResult.body.multiplied) + ")"}
    result: logResult
- returnResult:
    return: ${logResult}
```

This will guide you to edit and deploy the workflow. Once done, click on `Execute`
to execute the workflow. You'll be presented with the details of the execution:

![Workflows execution](/img/2020/workflows-execution.png)

Notice the status code `200`.

We just integrated an external service into our workflow, super cool!

## Connect an authenticated Cloud Run service

In the last sample, let's finalize our workflow with a call to a Cloud Run
service. To make it more interesting, deploy an internal Cloud Run service. This
means that the workflow needs to be authenticated to call the Cloud Run service.

The service
[floor](https://github.com/meteatamel/workflow-samples/tree/master/floor)
returns `math.floor` of the passed in number:

```python
import json
import logging
import os
import math

from flask import Flask, request

app = Flask(__name__)

@app.route('/', methods=['POST'])
def handle_post():
    content = json.loads(request.data)
    input = float(content['input'])
    return f"{math.floor(input)}", 200

if __name__ != '__main__':
    # Redirect Flask logs to Gunicorn logs
    gunicorn_logger = logging.getLogger('gunicorn.error')
    app.logger.handlers = gunicorn_logger.handlers
    app.logger.setLevel(gunicorn_logger.level)
    app.logger.info('Service started...')
else:
    app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
```

As you might know, Cloud Run deploys containers, so you need a `Dockerfile` and
your container needs to bind to `0.0.0.0` and `PORT` env variable, hence the
code above.

You can use the following
[Dockerfile](https://github.com/meteatamel/workflow-samples/blob/master/floor/Dockerfile).

Build the container:

```sh
export SERVICE_NAME=floor
gcloud builds submit --tag gcr.io/${PROJECT_ID}/${SERVICE_NAME}
```

Deploy to Cloud Run. Notice the `no-allow-unauthenticated` flag. This makes sure
the service only accepts authenticated calls:

```sh
gcloud run deploy ${SERVICE_NAME} \
  --image gcr.io/${PROJECT_ID}/${SERVICE_NAME} \
  --platform managed \
  --no-allow-unauthenticated
```

### Service Account for Workflows

Before we can configure Workflows to call the Cloud Run service, we need to create a service account for Workflows to use:

```sh
export SERVICE_ACCOUNT=workflows-sa
gcloud iam service-accounts create ${SERVICE_ACCOUNT}
```

Grant `run.invoker` role to the service account. This will allow the service
account to call authenticated Cloud Run services:

```sh
export PROJECT_ID=$(gcloud config get-value project)
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
    --member "serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
    --role "roles/run.invoker"
```

### Update the workflow

Update the `workflow.yaml` to include the Cloud Run service. Notice how we're
also including `auth` field to make sure Workflows passes in the authentication
token in its calls to the Cloud Run service:

```yaml
- randomgenFunction:
    call: http.get
    args:
        url: https://us-central1-workflows-atamel.cloudfunctions.net/randomgen
    result: randomgenResult
- multiplyFunction:
    call: http.post
    args:
        url: https://us-central1-workflows-atamel.cloudfunctions.net/multiply
        body:
            input: ${randomgenResult.body.random}
    result: multiplyResult
- logFunction:
    call: http.get
    args:
        url: https://api.mathjs.org/v4/
        query:
            expr: ${"log(" + string(multiplyResult.body.multiplied) + ")"}
    result: logResult
- floorFunction:
    call: http.post
    args:
        url: https://floor-wvdg6hhtla-ew.a.run.app
        auth:
            type: OIDC
        body:
            input: ${logResult.body}
    result: floorResult
- returnResult:
    return: ${floorResult}
```

Update the workflow. This time passing in the `service-account`:

```sh
gcloud beta workflows deploy workflow \
    --source=workflow.yaml \
    --service-account=${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com
```

Execute the workflow:

```sh
gcloud beta workflows execute workflow
```

In a few seconds, you can take a look at the workflow execution to see the
result. It should be an integer number returned from the floor function.

---

## Wrap up

Hopefully, this post helped you to have a good idea on what Workflows is. I'm
definitely impressed with the potential of this new product. I'll try out some
more complicated use cases in a future blog post.

In the meantime, if you want to check it out, here are some links:

* [Workflows overview](https://cloud.google.com/workflows)
* [Workflows docs](https://cloud.google.com/workflows/docs)
* [Quickstarts](https://cloud.google.com/workflows/docs/quickstarts)
* [workflow-samples](https://github.com/meteatamel/workflow-samples) repo
  with the code in this post

Feel free to reach out to me on Twitter
[@meteatamel](https://twitter.com/meteatamel) or read my previous posts on
[medium/@meteatamel](https://medium.com/@meteatamel).

