Use event notification to summarize PDF files on upload

Last updated Oct 12, 2026 In this tutorial, you will learn how to use event notifications to process a PDF file when it is uploaded to an R2 bucket. You will use Workers AI to summarize the PDF and store the summary as a text file in the same bucket.

Prerequisites

To continue, you will need:

<details>

<summary>

Node.js version manager

</summary>

Use <a href="https://viteplus.dev/guide/env">Vite+ (<code>vp env</code>) ↗︎</a> to install and manage Node.js versions.

For supported Node.js versions, refer to the <a href="https://developers.cloudflare.com/workers/wrangler/install-and-update/#install-wrangler">Wrangler system requirements</a>.

</details>

1. Create a new project

You will create a new Worker project that will use Static Assets to serve the front-end of your application. A user can upload a PDF file using this front-end, which will then be processed by your Worker.

Create a new Worker project by running the following commands:

npmyarnpnpm

npm create cloudflare@latest -- pdf-summarizer
yarn create cloudflare pdf-summarizer
pnpm create cloudflare@latest pdf-summarizer

For setup, select the following options:

  • For What would you like to start with?, choose Hello World example.
  • For Which template would you like to use?, choose Worker only.
  • For Which language do you want to use?, choose TypeScript.
  • For Do you want to use git for version control?, choose Yes.
  • For Do you want to add an AGENTS.md file to help AI coding tools understand Cloudflare APIs?, choose Yes.
  • For Do you want to deploy your application?, choose No (we will be making some changes before deploying).

Navigate to the pdf-summarizer directory:

cd pdf-summarizer

2. Create the front-end

Using Static Assets, you can serve the front-end of your application from your Worker. To use Static Assets, you need to add the required bindings to your Wrangler file.

{
    "assets": {
        "directory": "public"
    }
}
[assets]
directory = "public"

Next, create a public directory and add an index.html file. The index.html file should contain the following HTML code:

<details>

<summary>

Select to view the HTML code

</summary>

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>PDF Summarizer</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                display: flex;
                flex-direction: column;
                min-height: 100vh;
                margin: 0;
                background-color: #fefefe;
            }
            .content {
                flex: 1;
                display: flex;
                justify-content: center;
                align-items: center;
            }
            .upload-container {
                background-color: #f0f0f0;
                padding: 20px;
                border-radius: 8px;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            }
            .upload-button {
                background-color: #4caf50;
                color: white;
                padding: 10px 15px;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 16px;
            }
            .upload-button:hover {
                background-color: #45a049;
            }
            footer {
                background-color: #f0f0f0;
                color: white;
                text-align: center;
                padding: 10px;
                width: 100%;
            }
            footer a {
                color: #333;
                text-decoration: none;
                margin: 0 10px;
            }
            footer a:hover {
                text-decoration: underline;
            }
        </style>
    </head>
    <body>
        <div class="content">
            <div class="upload-container">
                <h2>Upload PDF File</h2>
                <form id="uploadForm" onsubmit="return handleSubmit(event)">
                    <input
                        type="file"
                        id="pdfFile"
                        name="pdfFile"
                        accept=".pdf"
                        required
                    />
                    <button type="submit" id="uploadButton" class="upload-button">
                        Upload
                    </button>
                </form>
            </div>
        </div>

<footer>
            <a
                href="https://developers.cloudflare.com/r2/buckets/event-notifications/"
                target="_blank"
                >R2 Event Notification</a
            >
            <a
                href="https://developers.cloudflare.com/queues/get-started/#3-create-a-queue"
                target="_blank"
                >Cloudflare Queues</a
            >
            <a href="https://developers.cloudflare.com/workers-ai/" target="_blank"
                >Workers AI</a
            >
            <a
                href="https://github.com/harshil1712/pdf-summarizer-r2-event-notification"
                target="_blank"
                >GitHub Repo</a
            >
        </footer>

<script>
            handleSubmit = async (event) => {
                event.preventDefault();

// Disable the upload button and show a loading message
                const uploadButton = document.getElementById("uploadButton");
                uploadButton.disabled = true;
                uploadButton.textContent = "Uploading...";

// get form data
                const formData = new FormData(event.target);
                const file = formData.get("pdfFile");

if (file) {
                    // call /api/upload endpoint and send the file
                    await fetch("/api/upload", {
                        method: "POST",
                        body: formData,
                    });

event.target.reset();
                } else {
                    console.log("No file selected");
                }
                uploadButton.disabled = false;
                uploadButton.textContent = "Upload";
            };
        </script>
    </body>
</html>

</details>

To view the front-end of your application, run the following command and navigate to the URL displayed in the terminal:

npm run dev
 ⛅️ wrangler 3.80.2
-------------------

⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:8787
╭───────────────────────────╮
│  [b] open a browser       │
│  [d] open devtools        │
│  [l] turn off local mode  │
│  [c] clear console        │
│  [x] to exit              │
╰───────────────────────────╯

When you open the URL in your browser, you will see that there is a file upload form. If you try uploading a file, you will notice that the file is not uploaded to the server. This is because the front-end is not connected to the back-end. In the next step, you will update your Worker that will handle the file upload.

3. Handle file upload

To handle the file upload, you will first need to add the R2 binding. In the Wrangler file, add the following code:

{
    "r2_buckets": [
        {
            "binding": "MY_BUCKET",
            "bucket_name": "<R2_BUCKET_NAME>"
        }
    ]
}
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "<R2_BUCKET_NAME>"

Replace <R2_BUCKET_NAME> with the name of your R2 bucket.

Next, update the src/index.ts file. The src/index.ts file should contain the following code:

src/index.tsts

export default {
    async fetch(request, env, ctx): Promise<Response> {
        // Get the pathname from the request
        const pathname = new URL(request.url).pathname;

if (pathname === "/api/upload" && request.method === "POST") {
            // Get the file from the request
            const formData = await request.formData();
            const file = formData.get("pdfFile") as File;

// Upload the file to Cloudflare R2
            const upload = await env.MY_BUCKET.put(file.name, file);
            return new Response("File uploaded successfully", { status: 200 });
        }

return new Response("incorrect route", { status: 404 });
    },
} satisfies ExportedHandler<Env>;

The above code does the following:

  • Check if the request is a POST request to the /api/upload endpoint. If it is, it gets the file from the request and uploads it to Cloudflare R2 using the Workers API.
  • If the request is not a POST request to the /api/upload endpoint, it returns a 404 response.

Since the Worker code is written in TypeScript, you should run the following command to add the necessary type definitions. While this is not required, it will help you avoid errors.

Prevent potential errors when accessing request.body

The body of a Request ↗︎ can only be accessed once. If you previously used request.formData() in the same request, you may encounter a TypeError when attempting to access request.body.

To avoid errors, create a clone of the Request object with request.clone() for each subsequent attempt to access a Request's body. Keep in mind that Workers have a memory limit of 128 MB per Worker and loading particularly large files into a Worker's memory multiple times may reach this limit. To ensure memory usage does not reach this limit, consider using Streams.

npm run cf-typegen

You can restart the developer server to test the changes:

npm run dev

4. Create a queue

Event notifications capture changes to data in your R2 bucket. You will need to create a new queue pdf-summarize to receive notifications:

npx wrangler queues create pdf-summarizer

Add the binding to the Wrangler file:

{
    "queues": {
        "consumers": [
            {
                "queue": "pdf-summarizer"
            }
        ]
    }
}