DEV Community

Cover image for Implementing Cloudflare Workflows
György Márk Varga for Shiwaforce

Posted on

Implementing Cloudflare Workflows

Serverless functions are great for short running tasks, but what happens when you need a multi-step process or a lengthy calculation? Often, it means piecing together complex workarounds or relying on third-party SaaS services.
Here comes Cloudflare Workflows into the picture – a native, streamlined solution that lets you orchestrate intricate workflows effortlessly. With Cloudflare Workflows, you can set up custom triggers, from any Cloudflare Worker, HTTP calls, or scheduled cron jobs, to kick off sequences that adapt to your specific needs.

In this article, we’ll dive into a project that blends AI and Cloudflare services to create something unique. I built an app that runs in the background and, every day at 7:00 PM, emails an AI-generated summary of the day’s events straight from our calendar. It was a fun challenge to put together, and I’m excited to walk you through the details!

The project

In this article, we look at a project that uses quite a lot of Cloudflare services. This app is working entirely in the background: at the end of each day, at 19:00, it sends an email with an AI-generated text summary of the events that happened on our calendar on that day.

This is how the e-mail looks like:

Image description

The tech stack

  • Typescript (of course 🙂)
  • Cloudflare Workers
  • Cloudflare Workflows
  • Cloudflare’s Meta AI model
  • Cloudflare Cron triggers
  • Google Calendar API
  • Resend (for sending e-mails)
  • React-Email with TailwindCSS (for beautiful emails)

The architecture

Image description

Source code

As with all my articles, the source code of the entire project can be found HERE, on Github.

The project implementation

Let's start the project walkthrough. The structure of the project looks like this:

Image description

It seems simple enough and yes, it is!

Let's first look at the index.ts file in the src folder.

At the top we can see the types:

type Env = {
    MAIN_WORKFLOW: Workflow;
    GOOGLE_CLIENT_ID: string;
    GOOGLE_CLIENT_SECRET: string;
    GOOGLE_REFRESH_TOKEN: string;
    RESEND_API_KEY: string;
    AI: Ai;
};

type Params = {
    startDate: string;
    endDate: string;
};

interface TokenResponse {
    access_token: string;
    expires_in: number;
    token_type: string;
    scope: string;
}

interface EventData {
    items: Array<{
        id: string;
        summary: string;
        description: string;
        start: { dateTime: string };
        end: { dateTime: string };
    }>;
}

interface AiResponse {
    response: string;
}
Enter fullscreen mode Exit fullscreen mode

The first type "Env" is the type of environment variables shown in the wrangler.toml file.
And the following types and interfaces are response types from Google and Meta LLama AI model.

Next, we can see a class called mainWorkFlow. You can find our 4-step workflow there. Let's go through them one by one, starting with the first step:

// Step 1: Retrieve Access Token
const accessToken = await step.do(
    "getAccessToken",
    { retries: { limit: 5, delay: 1000 } },
    async () => {
        const response = await fetch("https://oauth2.googleapis.com/token", {
            method: "POST",
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            body: new URLSearchParams({
                client_id: this.env.GOOGLE_CLIENT_ID,
                client_secret: this.env.GOOGLE_CLIENT_SECRET,
                refresh_token: this.env.GOOGLE_REFRESH_TOKEN,
                grant_type: "refresh_token",
            }),
        });

        if (!response.ok) throw new Error("Failed to refresh access token");
        const tokenData = (await response.json()) as TokenResponse;
        return tokenData.access_token;
    }
);
Enter fullscreen mode Exit fullscreen mode

As you can see, we can specify the name and configuration of each step (for example, how many times we try to retry them if they fail).
In this first step, we request the access token from Google Oauth based on our refresh token and client data, which we can later use to query the events of the given day.
And that brings us to the next step.
Step two looks like this:

// Step 2: Fetch Calendar Events within Date Range
const events = await step.do(
    "fetchMeetings",
    { retries: { limit: 3, delay: 2000 } },
    async () => {
        const response = await fetch(
    `https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${startDate}&timeMax=${endDate}&orderBy=startTime&singleEvents=true`,
            {
                headers: { Authorization: `Bearer ${accessToken}` },
            }
        );

        if (!response.ok) throw new Error("Failed to fetch events");
        const data = (await response.json()) as EventData;
        return data.items.map((event) => ({ summary: event.summary, description: event.description }));
    }
);
Enter fullscreen mode Exit fullscreen mode

Here, we very simply call the Google API and query the meetings based on the specified date range (today). We are particularly interested in the summary and description. We will give these as prompts for our AI model. This will be the third step.

In the third step, we use the Meta Llama 3.1 model provided by Cloudflare to generate a cool little summary text about the day behind us. We don't even need to use streaming here, since this is a Workflow, we have time and also we can't stream e-mails (yet).

// Step 3: Generate Summary using AI
const descriptions = events.map(event => event.description).join("\n");
const aiResponse = await step.do(
    "generateSummary",
    { retries: { limit: 3, delay: 2000 } },
    async () => {
        const messages = [
            { role: "system", content: "You are a friendly assistant" },
            { role: "user", content: `Generate a playful summary of the user's week based on these event description:\n${descriptions}. Please just write out the playful summary, nothing more!` },
        ];
        // @ts-ignore
        return await this.env.AI.run("@cf/meta/llama-3.1-70b-instruct", { messages });
    }
) as AiResponse;

Enter fullscreen mode Exit fullscreen mode

And in the fourth and last step, there is nothing left but to send our beautiful email with our wonderful summary:

// Step 4: Send Email using Resend
const resend = new Resend(this.env.RESEND_API_KEY);
await step.do(
    "sendEmail",
    { retries: { limit: 3, delay: 2000 } },
    async () => {
        const { data, error } = await resend.emails.send({
            from: 'Gyuri <hello@gyorgymarkvarga.com>',
            to: ['gyorgy.varga@shiwaforce.com'],
            subject: 'Weekly Summary',
            react: SummaryEmail({ meetingSummary: aiResponse.response })
        });

        if (error) {
            console.error({ error });
            throw new Error("Failed to send email");
        }
    }
);
Enter fullscreen mode Exit fullscreen mode

Here we use Resend with React Email, which allows us to send emails in a very simple, developer-friendly way. Of course feel free to modify the email addresses. :)

Now you wonder how we can make sure to trigger that workflow every working day at 19:00. Here is the code snippet that is responsible for that:

const startWorkflow = async (env: Env) => {
    const today = new Date();
    const firstDayOfWeek = new Date(today.setDate(today.getDate() - today.getDay()));

    const instance = await env.MAIN_WORKFLOW.create({
        params: { startDate: firstDayOfWeek.toISOString(), endDate: new Date().toISOString() },
    });

    return new Response(JSON.stringify(await instance.status()), {
        headers: { "content-type": "application/json" },
    });
}

export default {
    async scheduled(
        env: Env,
        ctx: ExecutionContext,
    ) {
        ctx.waitUntil(startWorkflow(env));
    },
};
Enter fullscreen mode Exit fullscreen mode

In here we have a startWorkFlow function that will fire up our workflow and we have a scheduled handler, that is needed for starting the workflow at a specific time (every working day at 7 pm).

Now let's see how to create beautiful emails based on React using React email. In the src folder inside the email folder there is a summary-email.tsx file, this will be the template for our email:

import * as React from 'react';
import { Html, Head, Body, Container, Section, Text, Button, Hr, Link, Tailwind } from '@react-email/components';

interface EmailProps {
    meetingSummary?: string;
}

export const SummaryEmail = ({ meetingSummary = "Your AI-generated meeting summary will appear here." }: EmailProps) => {
    return (
        <Html>
            <Head />
            <Tailwind>
                <Body className="bg-white font-sans">
                    <Container className="mx-auto px-6 py-8 max-w-2xl">
                        <Text className="text-3xl font-bold text-center text-black mb-6">
                            Your meetings summary for the day
                        </Text>
                        <Text className="text-lg text-gray-700 mb-4">Hello,</Text>
                        <Text className="text-lg text-gray-700 mb-4">
                            Here's a summary of your meetings for this day:
                        </Text>
                        <Section className="bg-gray-100 rounded-lg p-6 mb-6">
                            <Text className="text-base text-gray-800 whitespace-pre-wrap">
                                {meetingSummary}
                            </Text>
                        </Section>
                        <Hr className="border-t border-gray-300 my-6" />
                        <Section className="text-center">
                            <Text className="text-lg text-gray-700 mb-4">
                                Need more details? Check your full calendar:
                            </Text>
                            <Button
                                href="https://calendar.google.com"
                                className="bg-black text-white py-3 px-6 rounded text-base font-medium no-underline inline-block"
                            >
                                View Calendar
                            </Button>
                        </Section>
                        <Hr className="border-t border-gray-300 my-6" />
                        <Text className="text-sm text-gray-500 text-center">
                            © 2024 My Calendar Summary, Inc. All rights reserved.
                            <br />
                            <Link href="https://example.com" className="text-blue-600 underline">
                                My Calendar Summary
                            </Link>
                        </Text>
                    </Container>
                </Body>
            </Tailwind>
        </Html>
    );
}

export default SummaryEmail;
Enter fullscreen mode Exit fullscreen mode

Here we can create a template as if we were creating a React component. With props, components and TailwindCSS. Do you need more than that?
We can even watch this in real time. Let's just start the preview with our "email" script in package.json:

"email": "email dev --dir ./src/emails"
Enter fullscreen mode Exit fullscreen mode

With this, we can see the previews of our e-mail template in real time with hot reload.

Another thing worth looking at is the wrangler.toml file:

name = "calendar-workflow"
main = "src/index.ts"
compatibility_date = "2024-10-22"
compatibility_flags = ["nodejs_compat"]


[observability]
enabled = true

[[workflows]]
name = "main-workflow"
binding = "MAIN_WORKFLOW"
class_name = "mainWorkflow"

[ai]
binding = "AI"

[triggers]
crons = ["0 19 * * 1-5"]
Enter fullscreen mode Exit fullscreen mode

and also the dev.vars file (which you can create for local development):

GOOGLE_CLIENT_ID = "<YOUR_GOOGLE_CLIENT_ID>"
GOOGLE_CLIENT_SECRET = "<YOUR_GOOGLE_CLIENT_SECRET>"
GOOGLE_REFRESH_TOKEN = "<YOUR_GOOGLE_REFRESH_TOKEN>"
RESEND_API_KEY = "<YOUR_RESEND_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

Here will be all the secrets that we need. In production you will need to add these secrets with the wrangler CLI. More info on that HERE.

Here, what is interesting to us are the environmental variables, here we will have to replace them with our own if we want to start the project ourselves.
Within [[workflows]], we define our workflows. We also enter the name of the class here.
We will also bind AI ​​in the next section.
And finally, we define triggers. This will ensure that every day, from Monday to Friday at 7pm, a cron job runs and starts our main workflow, which does the Google API authentication, then retrieves the calendar events for the given day, makes an AI summary and then it sends it via e-mail.

How to deploy it?

  1. First register to Resend and get an API key. You can get up and running in no time HERE.
  2. Register to Google Developers Console and get a client id and client secret, you can follow THIS guide.
  3. Generate the refresh token HERE. On the top right you can set your client id and also client secret from the second step Image description And you must set the calendar.readonly right on the left and you can generate your tokens Image description
  4. Now we have all the data that can be inserted into our .dev.vars file and also into the Cloudflare environment variables with Wrangler CLI, more that HERE. Probably you do need to do this after the last step if you haven't got Wrangler CLI installed in your project.
  5. Pull the project from the Github repo.
  6. Run npm install on route.
  7. Follow THIS guide to deploy it to Cloudflare. You can skip the first step, because we already have a project.

Let' connect!

I hope this article was useful. If you have any questions, you can always find me here in the comment section or on X and we can talk! :)
I should also mention that this post was made possible thanks to Shiwaforce, where I’ve had the chance to work on projects like this and bring ideas to life.

Top comments (0)