Cloudflare launched Project Think on April 15, 2026, a durable multi-tenant runtime for AI agents. Each agent gets a sandboxed JavaScript worker, persistent state, and tool calling, with no outbound network by default. pdf-lib runs cleanly inside. Playwright and Chromium do not. The canonical pattern for HTML-to-PDF work is to keep the rendering service outside the sandbox and call it through a tool registered with the orchestrator.
This article walks through what Project Think actually ships, why the sandbox model forks PDF workflows into two architectures, and which libraries survive in practice. The audience is developers building AI agents on Cloudflare's stack and architects choosing where document generation should live in an agent system.
What Project Think actually ships
Project Think is a durable agent runtime built on top of Cloudflare's existing isolate infrastructure. The launch post describes it as a way to "run agents with persistent memory, deterministic tool calling, and per-tenant isolation, without standing up your own runtime." Each agent is a sandboxed JavaScript worker that the platform keeps warm across turns, with a typed tool registry the orchestrator exposes to the agent.
Two tiers exist:
| Tier | Network | Modules | Use case |
|---|---|---|---|
| Tier-1 (Sandboxed) | None outbound, can call orchestrator only | Fixed module surface, no npm install | Deterministic agents, regulated workloads |
| Tier-2 (Dynamic Workers) | Same default policy, plus orchestrator-mediated egress | npm install at request time, pure JS only | Flexible agents, library experimentation |
Both tiers run on the same workerd-derived sandbox as Cloudflare Workers. That means no native binaries, no shared libraries, no child_process.spawn, no filesystem outside the agent's durable state. The Workers runtime docs describe the model in detail: V8 isolates, a curated Web API surface, no Node-style escape hatches.
The persistence story is the part that matters for PDF workflows. Each agent owns a durable state object. State survives across turns, across cold starts, and across the multi-tenant scheduler reassigning the agent to a different physical machine. Tool results, intermediate work, and small binary payloads can all sit there, with the platform handling replication.
Think of Project Think as Durable Objects, but with an agent loop on top, a typed tool registry, and a default-deny network policy. Everything you knew about Cloudflare Workers' sandbox still applies inside the agent. The agent loop is the new layer.
Why this matters for PDF workflows
The most common AI-agent PDF use case is a three-step loop: the agent reads structured input through a tool, reasons about it, and produces a PDF as the user-visible artefact. Examples we see in production every week: a contract review agent that summarises a 40-page PDF and returns a one-page brief, a finance agent that builds a monthly receipt from raw transactions, a support agent that turns a chat transcript into a printable case summary.
If the agent runtime cannot render PDFs natively, the architecture forks. Either the rendering happens inside the sandbox using a pure-JS library, with the format constraints that implies, or the agent calls a rendering service outside the sandbox through a tool. Both patterns are valid. The choice depends on whether your PDFs need CSS-grade layout or whether positioned text and drawn rectangles are enough.
Project Think makes this fork explicit by default. There is no third option where the agent "just opens a headless browser." That door is closed at the runtime level.
Test 1: does pdf-lib work?
Yes. pdf-lib is a pure-JavaScript PDF construction library with no native dependencies and no DOM requirements. It installs in Tier-2, runs in both tiers, and produces valid PDF bytes.
A minimal Project Think tool that builds a one-page receipt looks like this:
import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
export const buildReceipt = {
name: "build_receipt",
description: "Build a single-page receipt PDF from line items",
handler: async ({ items, total }: ReceiptInput) => {
const pdf = await PDFDocument.create();
const page = pdf.addPage([612, 792]);
const font = await pdf.embedFont(StandardFonts.Helvetica);
page.drawText("Receipt", { x: 50, y: 740, size: 24, font });
let y = 700;
for (const item of items) {
page.drawText(`${item.name} ${item.price}`, { x: 50, y, size: 12, font });
y -= 16;
}
page.drawText(`Total: ${total}`, { x: 50, y: y - 20, size: 14, font });
const bytes = await pdf.save();
return { pdf: Buffer.from(bytes).toString("base64") };
},
};What you give up by going this route is anything that needs a browser: CSS layout, web fonts loaded over the network, @media print, flexbox, grid, SVG that depends on DOM measurements, and any chart library that draws to a canvas the browser owns. Tables work if you position cells yourself. Charts work if you draw them as polylines and rectangles. Wrapping text needs manual measurement against the font metrics pdf-lib exposes.
For receipts, simple invoices, certificates with one piece of dynamic text, and any document that fits a fixed template, pdf-lib inside the sandbox is fine. For HTML-driven layout, it is not.
Test 2: does Playwright or Puppeteer work?
No, with no path around it. Both libraries depend on child_process.spawn to launch a Chromium binary, and the Project Think sandbox does not expose child_process. Attempting to import Puppeteer in a Tier-2 worker fails at install time on a native peer dependency. Importing the JS portion alone and trying to call puppeteer.launch() throws on the first call to spawn.
import puppeteer from "puppeteer";
// Tier-2 install fails: native peer dep rejected
// Even if you could install: launch() throws because child_process is undefined
await puppeteer.launch();
// TypeError: Cannot read properties of undefined (reading 'spawn')Playwright fails in the same place for the same reason. The Chromium binary distributed by Playwright is a native ELF executable, not a JavaScript module. The sandbox has no dynamic loader, no ld.so, and no syscall to start a new process. There is no V8 flag, no opt-in, no workaround. The constraint is at the kernel boundary of the workerd isolate.
The official Cloudflare position, stated on the Workers AI docs, is that browser-driven rendering belongs in Cloudflare's Browser Rendering API, which is a separate service that runs full Chromium outside the isolate.
Test 3: does Browser Rendering work from a Think agent?
This is the architecturally interesting case. Cloudflare's Browser Rendering is a managed service that gives Workers and Pages projects an HTTP-style binding to a real headless Chromium pool. The natural question is whether a Project Think agent can use that binding.
As of the launch post, the answer is "yes in Tier-2, with caveats." Tier-2 Dynamic Workers can declare bindings to other Cloudflare services, and Browser Rendering is one of them. Tier-1 cannot, because Tier-1 has no outbound surface at all, including to first-party Cloudflare services. The agent in Tier-2 calls env.BROWSER.fetch(...) exactly the way a regular Worker would, gets back the rendered PDF bytes, and stashes them in durable state or pushes them to R2.
What this means in practice: if your agent needs full Chromium fidelity and you want everything on Cloudflare, run the agent in Tier-2 and bind to Browser Rendering. If you are in Tier-1 for compliance reasons, you cannot reach Browser Rendering from inside the agent. You have to model the render as a tool that the orchestrator runs outside the sandbox.
The canonical pattern: render outside the sandbox
The pattern that works across every tier of Project Think, every other agent runtime, and every regulated environment, is to keep the rendering service outside the sandbox. The agent reasons in the sandbox. The orchestrator runs the render. The bytes come back to the agent as a tool result.
The flow is the same as any other tool call. The agent emits a structured tool invocation. The orchestrator validates and executes it against an external service. The service returns the rendered bytes, typically as base64 or a signed URL. The agent stores the result in durable state and continues the loop.
The renderer outside the sandbox can be:
- Cloudflare Browser Rendering, called from the orchestrator
- An external API like PDF4.dev, called over HTTPS with an API key
- A self-hosted Playwright or Gotenberg pool, called over a private network
All three look identical to the agent. The decision is operational, not architectural. PDF4.dev's render endpoint returns either a binary body, a base64 payload, or a signed URL valid for 24 hours, which is the shape that fits this pattern most naturally.
Where Project Think fits in the agent landscape
Four agent runtimes shipped in the last year. They have different defaults for the four dimensions that decide whether PDF rendering lives inside or outside:
| Runtime | Native PDF rendering | Network default | Durable state | Notes |
|---|---|---|---|---|
| Cloudflare Project Think | Pure-JS only, no Chromium | Deny outbound, orchestrator-mediated | Yes, per-agent | Tier-2 binds to Browser Rendering |
| Anthropic Computer Use | Yes via a real browser | Open by design, the GUI is the point | No, ephemeral VM | Closest thing to a desktop agent |
| OpenAI Operator | Yes via a real browser | Open by design | Session-scoped | Driven by GPT-4o, GUI-focused |
| Microsoft AutoGen runtime | Library choice | Host-dependent | Host-dependent | Framework, not a managed runtime |
Anthropic's Computer Use and OpenAI's Operator both ship a real browser in a VM. PDF rendering inside those agents is whatever the browser's "Print to PDF" produces, with all the fidelity and all the cost that implies. They are GUI agents first, headless tools second.
Project Think sits at the opposite end. No GUI, no browser, deterministic sandbox, per-tenant isolation. PDF rendering is a tool, not a built-in capability. For backend agent workloads, regulated industries, and any system where "the agent should not be able to access the public internet by default" is a hard requirement, that posture is the point. For UI automation, it is not the right tool.
Practical implementation
A Project Think agent that reads a contract and produces a PDF summary looks like this. The agent itself does no rendering. It calls read_contract to pull the document into context, drafts the summary in the model, then calls render_pdf to produce the artefact.
import { defineAgent } from "@cloudflare/think";
export default defineAgent({
name: "contract-summariser",
tools: {
read_contract: async ({ contract_id }) => {
const r = await env.CONTRACTS.get(contract_id);
return { text: await r.text() };
},
render_pdf: async ({ template_id, data }) => {
const resp = await fetch("https://pdf4.dev/api/v1/render", {
method: "POST",
headers: {
"Authorization": `Bearer ${env.PDF4_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ template_id, data, delivery: "url" }),
});
return await resp.json();
},
},
system: `You summarise contracts.
Step 1: call read_contract with the id the user provides.
Step 2: draft a 200-word summary.
Step 3: call render_pdf with template_id "contract_summary" and data { title, parties, summary }.
Step 4: return the signed URL to the user.`,
});Two things to call out in this pattern. First, render_pdf runs in the orchestrator, not in the sandbox. The agent only sees the tool signature and the result. The fact that there is an HTTP call to a third-party service is invisible to the model. Second, the result is a signed URL with a 24-hour TTL, not a base64 blob. Storing URLs in durable state is cheap. Storing raw PDF bytes there is not.
Swap the render_pdf body for env.BROWSER.fetch(...) in Tier-2 and the agent code is identical. Swap it for pdf-lib and the rendering moves into the sandbox, with the format limits that brings. The agent does not care which renderer runs underneath the tool.
Bottom line
Project Think is a sandboxed agent runtime, not a browser. Pure-JS PDF construction with pdf-lib runs inside. Chromium-based rendering with Playwright or Puppeteer does not, by design. For agents that need HTML-grade output, the rendering belongs outside the sandbox, called through the same tool layer the agent already uses for every other side effect. That is the canonical pattern Cloudflare wants you to adopt, and it is the pattern that ports cleanly to every other deny-by-default agent runtime that ships after this one.
If you already use PDF4.dev's render endpoint from a Cloudflare Worker, the migration to Project Think is a one-line tool registration. The same is true for any HTTP-based renderer. The hard work, as always, is on the template side, not the runtime side.
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



