Supercharge Your AI Chatbot: Trigger Long-Running Tasks with Cloudflare Workflows
Cloudflare Developer Week 2025
Cloudflare Developer week is this week and they're dropping some BANGERS. Some of my favorites:
- Use Vite with Workers which will make it simple to develop workers locally
- RealtimeKit - a collection of SDKs for working with their WebRTC services (I had to basterdize Twilio's WebRTC for a project and will be excited to migrate into this).
- OAuth MCP to authenticate MCP calls via your own OAuth implementation or something like WorkOS. By the way, I wrote a package to help Laravel developers integrate Organizations from WorkOS (or Teams) into Laravel called Laravel WorkOS Teams.
- Durable Objects are now in the free tier!
- And finally, Workflows are now in GA.
I have a need to run a Workflow from an AI Chat as a tool, so I decided to play around with the new releases and chew on some glass.
I ran into a few issues along the way, so I figured I document it and share with you today.
Challenges We'll Talk About In This Article
Cloudflare released an Agents Starter template to get agents up and running fast. It comes with some baked in tools to show you how it all comes together and is a beautiful starting point. Here's the issues I ran into when trying to implement Workflows into my agent:
Env
External Workflows are binded in the wrangler.jsonc file and need to be passed into the agent Env. I had to come up with a path to then pass along my binding to the toolset its supposed to call.
Local Development of Agents Calling Workflows
Once you bind a Workflow service, using npm run start
for your local development process will fail because it cannot find the Workflow service. So you'll be tempted to run npx wrangler dev --remote
. Well, SQLite is required by the Agent Starter template, and is not available in remote - so you will get a 500 error.
Next you'll be tempted to run npx wrangler dev --local
. But that won't be able to find your external Workflow. Meaning, you'll want to create and test your Workflow separately over REST, and then you'll have to test it's integration with a live deployment in your agent. Once you know it's working, I will show you how to override your binding so that npm run start
in a way that won't care about the Workflow service binding.
Let's get started.
Introduction
Cloudflare Agents provide a powerful way to build AI-driven applications directly on the edge. Combined with tools, your AI agent can interact with external services, fetch data, and perform actions. But what happens when an action isn't instantaneous? Tasks like web scraping, report generation, or data processing can take significant time, potentially exceeding Worker execution limits.
This is where Cloudflare Workflows shine! Workflows allow you to define, orchestrate, and monitor complex, long-running tasks reliably. In this post, we'll explore how to bridge the gap between your AI Agent chat interface and these powerful workflows, allowing a user to trigger a long-running task directly from a chat message.
We'll modify the official Cloudflare Agents Starter template to add a new tool that triggers an example "SEO Scraper" workflow deployed as a separate Worker.
Prerequisites:
- A Cloudflare account.
- Node.js and npm/yarn installed.
- Wrangler CLI installed and configured.
- Basic familiarity with Cloudflare Workers, Agents, and the concept of Workflows.
- The Agents Starter template cloned locally (
agents-starter
). - A separate Cloudflare Workflow project (e.g.,
seo-scraper
) deployed, which contains theSeoScraperWorkflow
class.
Step 1: Define Your Workflow
First, ensure your Cloudflare Workflow is defined and deployed. For this example, we have a separate project named seo-scraper
containing our workflow logic (likely in a file like src/workflows.ts
) with an entrypoint class SeoScraperWorkflow
. This workflow should be designed to accept parameters (like a url
) passed during instance creation.
Step 2: Bind the Workflow in wrangler.jsonc
To make the external workflow accessible from our Agent Worker (agents-starter
), we need to create a binding in the top level of the agents-starter/wrangler.jsonc
file.
{
"name": "agents-starter",
"main": "src/server.ts",
"compatibility_date": "2025-02-04",
// ... other top-level config ...
"durable_objects": {
"bindings": [
{ "name": "Chat", "class_name": "Chat" }
]
},
"workflows": [ // <--- Top-level workflow binding for production/remote dev
{
"name": "seo-scraper-workflow", // Deployed Workflow service name
"binding": "SEO_SCRAPER_WORKFLOW", // Variable name in Worker code
"class_name": "SeoScraperWorkflow", // Workflow's entrypoint class
"script_name": "seo-scraper" // Worker script containing the workflow
}
],
"migrations": [ /* ... */ ],
"observability": { "enabled": true }
// ... env section etc. ...
}
name
: The name you gave your workflow during its deployment.binding
: How you'll refer to this workflow binding in your Agent Worker's code (e.g.,env.SEO_SCRAPER_WORKFLOW
).class_name
: The exported class name from your workflow code.script_name
: This points to the Worker service where the workflow is deployed.
After adding this, run wrangler types
in the agents-starter
directory to update worker-configuration.d.ts
.
Step 3: Define the AI Tool in src/tools.ts
Define a tool for the AI to use, specifying the parameters it needs.
import { tool } from "ai";
import { z } from "zod";
// ... other imports ...
const scrapeWebsite = tool({
description: "scrape a given url using the SEO_SCRAPER_WORKFLOW and return the workflow instance ID",
parameters: z.object({
url: z.string().url().describe("The valid URL of the website to scrape"),
}),
// No 'execute' function = requires human confirmation
});
export const tools = {
// ... other tools ...
scrapeWebsite,
};
export const executions = {
// ... other executions ...
// Logic for scrapeWebsite will be added in the next step
};
Step 4: Provide Environment Context and Implement Execution
Use AsyncLocalStorage
to pass the environment binding to the tool's execution logic.
Update src/server.ts
:
// ... imports ...
import type { Workflow } from "cloudflare:workers"; // Import Workflow type
// Update context type
export const agentContext = new AsyncLocalStorage<{
chat: Chat;
SEO_SCRAPER_WORKFLOW: Workflow; // Include the workflow binding type
}>();
export class Chat extends AIChatAgent<Env> { // Use updated Env
async onChatMessage(onFinish: StreamTextOnFinishCallback<{}>) {
// Pass context, including the workflow binding from the environment
return agentContext.run({
chat: this,
SEO_SCRAPER_WORKFLOW: this.env.SEO_SCRAPER_WORKFLOW // Pass it here!
}, async () => {
const dataStreamResponse = createDataStreamResponse({
execute: async (dataStream) => {
const processedMessages = await processToolCalls({
messages: this.messages, dataStream, tools, executions,
});
// ... streamText call ...
}
});
return dataStreamResponse;
});
}
// ... other methods ...
}
// ... default export ...
Implement the execution logic in /agents-starter/src/tools.ts
:
import { agentContext } from "./server";
// ... other imports ...
export const executions = {
// ... other executions ...
async scrapeWebsite({ url }: { url: string }) {
const store = agentContext.getStore();
// Check if the binding exists in the current context
// This check is important for the local dev workaround later!
if (!store || !store.SEO_SCRAPER_WORKFLOW) {
console.error("Workflow binding 'SEO_SCRAPER_WORKFLOW' not found in agent context. Likely running in local dev without the binding.");
return "Error: Workflow execution is not available in this environment.";
}
try {
const workflowBinding = store.SEO_SCRAPER_WORKFLOW;
const instance = await workflowBinding.create({
id: `scrape-${crypto.randomUUID()}`,
params: { url: url },
});
console.log(`Scraping workflow started for ${url}. Instance ID: ${instance.id}`);
return `OK, I've started scraping ${url}. The workflow instance ID is ${instance.id}.`;
} catch (error: any) {
console.error("Error starting workflow:", error);
return `Error: Failed to start the scraping workflow. ${error.message}`;
}
},
};
// Ensure tools object includes scrapeWebsite
export const tools = {
getWeatherInformation,
getLocalTime,
scheduleTask,
scrapeWebsite,
};
Step 5: Update Frontend for Confirmation in src/app.tsx
Add the tool name to toolsRequiringConfirmation
:
const toolsRequiringConfirmation: (keyof typeof tools)[] = [
"getWeatherInformation",
"scrapeWebsite", // <-- Added
];
// ... rest of component ...
Step 6: Handling Local Development - UI Focus vs. End-to-End Testing
Testing Workers that depend on other Cloudflare services (like Durable Objects or external Workflows/Workflows) locally presents a choice:
Scenario A: Focusing on UI/UX
You noticed that simply running npm start
fails. This happens because the local dev server tries, and fails, to resolve the SEO_SCRAPER_WORKFLOW
binding defined at the top level of wrangler.jsonc
.
My clever workaround leverages how wrangler.jsonc
environments work: bindings defined at the top level are NOT automatically inherited by sections inside env
.
By defining only the necessary bindings for the core agent functionality (like the Chat
Durable Object) inside env.dev
, you effectively omit the workflows
binding for the local development environment triggered by npm start
.
{
// ... name, main, top-level bindings (DO, Workflows) ...
"env": {
"dev": {
// We ONLY redefine the bindings essential for the *agent itself* to run locally.
"durable_objects": {
"bindings": [
{ "name": "Chat", "class_name": "Chat" }
]
}
// --> NOTICE: We DO NOT redefine the "workflows" binding here <--
}
},
// ...
}
Define your environment when starting
CLOUDFLARE_ENV=dev npm run start
Outcome:
CLOUDFLARE_ENV=dev npm run start
now works without crashing.- The chat interface loads successfully in your browser.
- You can test and develop UI elements, state management, and other tools that don't rely on the workflow binding.
- If you try to trigger the
scrapeWebsite
tool, theagentContext.getStore()
call insrc/tools.ts
will find thatstore.SEO_SCRAPER_WORKFLOW
is undefined. Your error handling (added in Step 4) will catch this, and the tool call will fail gracefully within the chat (e.g., showing "Error: Workflow execution is not available...") instead of crashing the Worker.
Scenario B: End-to-End Testing (Requires deployment)
To test the complete flow – from chat input to the AI deciding to use the tool, user approval, and the actual triggering of the deployed workflow – you still need to ensure the binding resolves correctly.
Since the workflow (seo-scraper
) is deployed as a separate service, and the Chat Agent requires SQLite, the only way for you to test your deployment is to deploy your code to Cloudflare.
Therefore, for full end-to-end testing, run:
wrangler deploy
This command uploads your agents-starter
code and runs it on Cloudflare's edge, where the SEO_SCRAPER_WORKFLOW
binding can be correctly resolved to the deployed seo-scraper
service. This allows you to verify the entire interaction.
Choosing Your Approach:
- Use the
env.dev
omission technique (Scenario A) for rapid UI/UX development and testing non-workflow features locally. - Use
wrangler dev --remote
(Scenario B) when you need to specifically test the workflow triggering mechanism and its integration end-to-end.
Step 7: Deploy
Deploy both services for production:
# In your Workflow project directory (e.g., seo-scraper)
wrangler deploy
# In your Agent Starter project directory (agents-starter)
wrangler deploy
Conclusion
Binding an external Cloudflare Workflow to your Agent Worker unlocks powerful capabilities for handling long-running tasks triggered by chat. By correctly configuring the binding in wrangler.jsonc
(using script_name
) and managing context via AsyncLocalStorage
, you create a robust integration. Understanding the nuances of local development – using the env.dev
omission trick for UI focus – is key to an efficient development workflow.