-
Notifications
You must be signed in to change notification settings - Fork 614
feat(think): dynamic ThinkWorkflow support via Dynamic Workers #1786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
fcaaee1
ddc8fa2
bfeb219
0571a09
48970f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| "@cloudflare/think": minor | ||
| --- | ||
|
|
||
| Add dynamic workflow support via `@cloudflare/think/dynamic-workflows`. | ||
|
|
||
| New API: | ||
|
|
||
| - `Think.runDynamicWorkflow(workflowName, code, params?, options?)` — stores generated TypeScript and starts a Dynamic Workflow instance | ||
| - `Think.getWorkflowCode(wfId)` — RPC method used by the loader to retrieve stored code | ||
| - `DynamicThinkWorkflow` — export from `@cloudflare/think/dynamic-workflows`, register as `class_name` in your `[[workflows]]` wrangler binding | ||
|
|
||
| Generated code extending `ThinkWorkflow` is bundled at runtime with `@cloudflare/worker-bundler` and executed as a Dynamic Worker with full durable execution (`step.prompt()`, `step.do()`, `step.sleep()`, `step.waitForEvent()`). | ||
|
|
||
| New dependencies: `@cloudflare/dynamic-workflows` (runtime), `@cloudflare/worker-bundler` (optional peer dep). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Dynamic Think Workflows | ||
|
|
||
| Run generated `ThinkWorkflow` code at runtime as Dynamic Workers with full durable execution (`step.prompt()`, `step.do()`, `step.sleep()`, `step.waitForEvent()`). | ||
|
|
||
| ## How it works | ||
|
|
||
| 1. `MyAgent.runDynamicWorkflow()` stores generated TypeScript source in SQLite and starts a Workflow instance | ||
| 2. The `DynamicThinkWorkflow` entrypoint (registered in `wrangler.jsonc`) loads the code from the agent, bundles it with its npm dependencies via `@cloudflare/worker-bundler`, and loads it as a Dynamic Worker | ||
| 3. The Workflows engine dispatches `run(event, step)` to the Dynamic Worker — `step.prompt()` works natively | ||
|
|
||
| ## Run it | ||
|
|
||
| ```bash | ||
| pnpm install | ||
| pnpm run dev | ||
| ``` | ||
|
|
||
| Then start a dynamic workflow: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:8787/run \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"topic": "The future of serverless computing"}' | ||
| ``` | ||
|
|
||
| ## Key concepts | ||
|
|
||
| - **Generated code** is plain TypeScript extending `ThinkWorkflow` — not a DSL or interpreter | ||
| - **`@cloudflare/dynamic-workflows`** handles routing between the Worker Loader and the Workflows engine | ||
| - **`@cloudflare/worker-bundler`** resolves npm dependencies (`@cloudflare/think`, `zod`) at runtime | ||
| - The generated workflow has full access to `step.prompt()`, `this.agent`, and all ThinkWorkflow features |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| { | ||
| "name": "@cloudflare/agents-dynamic-think-workflows-example", | ||
| "description": "Dynamic ThinkWorkflow example — run generated workflow code at runtime", | ||
| "private": true, | ||
| "type": "module", | ||
| "version": "0.0.0", | ||
| "scripts": { | ||
| "dev": "wrangler dev", | ||
| "deploy": "wrangler deploy", | ||
| "types": "wrangler types env.d.ts --include-runtime false" | ||
| }, | ||
| "dependencies": { | ||
| "@cloudflare/think": "*", | ||
| "agents": "*", | ||
| "ai": "^6.0.202", | ||
| "workers-ai-provider": "^3.2.0", | ||
| "zod": "^4.4.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@cloudflare/workers-types": "^4.20260612.1", | ||
| "typescript": "^6.0.3", | ||
| "wrangler": "^4.100.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { getAgentByName, routeAgentRequest } from "agents"; | ||
| import { createWorkersAI } from "workers-ai-provider"; | ||
| import { Think } from "@cloudflare/think"; | ||
| import { DynamicThinkWorkflow } from "@cloudflare/think/dynamic-workflows"; | ||
|
|
||
| export { DynamicThinkWorkflow }; | ||
|
|
||
| type Env = { | ||
| AI: Ai; | ||
| LOADER: WorkerLoader; | ||
| MyAgent: DurableObjectNamespace<MyAgent>; | ||
| DYNAMIC_THINK_WF: Workflow; | ||
| }; | ||
|
|
||
| /** | ||
| * A simple Think agent that can launch dynamic workflows. | ||
| */ | ||
| export class MyAgent extends Think<Env> { | ||
| getModel() { | ||
| return createWorkersAI({ binding: this.env.AI })( | ||
| "@cf/moonshotai/kimi-k2.7-code" | ||
| ); | ||
| } | ||
|
|
||
| getSystemPrompt() { | ||
| return "You are a helpful assistant."; | ||
| } | ||
|
|
||
| /** | ||
| * Generate and run a dynamic ThinkWorkflow. | ||
| * In a real app, this code could be LLM-generated, loaded from a DB, | ||
| * or authored by users at runtime. | ||
| */ | ||
| async startDynamicWorkflow(topic: string): Promise<{ workflowId: string }> { | ||
| const code = generateWorkflowCode(); | ||
| const workflowId = await this.runDynamicWorkflow("DYNAMIC_THINK_WF", code, { | ||
| topic | ||
| }); | ||
| return { workflowId }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Generate a ThinkWorkflow class as a TypeScript string. | ||
| * | ||
| * The generated code extends ThinkWorkflow and uses step.prompt() for | ||
| * durable LLM calls. It's bundled at runtime by worker-bundler and | ||
| * executed as a Dynamic Worker. | ||
| */ | ||
| function generateWorkflowCode(): string { | ||
| return ` | ||
| import { ThinkWorkflow } from "@cloudflare/think/workflows"; | ||
| import type { ThinkWorkflowStep } from "@cloudflare/think/workflows"; | ||
| import type { AgentWorkflowEvent } from "agents/workflows"; | ||
| import { z } from "zod"; | ||
|
|
||
| type Params = { topic: string }; | ||
|
|
||
| const summarySchema = z.object({ | ||
| title: z.string(), | ||
| summary: z.string(), | ||
| keyPoints: z.array(z.string()) | ||
| }); | ||
|
|
||
| export default class GeneratedWorkflow extends ThinkWorkflow { | ||
| async run( | ||
| event: AgentWorkflowEvent<Params>, | ||
| step: ThinkWorkflowStep | ||
| ): Promise<void> { | ||
| const result = await step.prompt("analyze", { | ||
| prompt: "Write a brief analysis about: " + event.payload.topic, | ||
| output: summarySchema, | ||
| timeout: "3 minutes" | ||
| }); | ||
|
|
||
| await step.do("save-result", async () => { | ||
| console.log("Analysis complete:", JSON.stringify(result, null, 2)); | ||
| }); | ||
| } | ||
| } | ||
| `.trim(); | ||
| } | ||
|
|
||
| async function getDefaultAgent(env: Env) { | ||
| return getAgentByName(env.MyAgent, "default"); | ||
| } | ||
|
|
||
| export default { | ||
| async fetch(request: Request, env: Env) { | ||
| const agentResponse = await routeAgentRequest(request, env); | ||
| if (agentResponse) return agentResponse; | ||
|
|
||
| const url = new URL(request.url); | ||
|
|
||
| if (request.method === "POST" && url.pathname === "/run") { | ||
| const body = (await request.json()) as { topic?: unknown }; | ||
| if (typeof body.topic !== "string" || body.topic.trim() === "") { | ||
| return Response.json( | ||
| { error: "Expected JSON body with 'topic' field" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
| const agent = await getDefaultAgent(env); | ||
| const result = await agent.startDynamicWorkflow(body.topic); | ||
| return Response.json(result); | ||
| } | ||
|
|
||
| return Response.json( | ||
| { | ||
| routes: ["POST /run { topic: string } — start a dynamic ThinkWorkflow"] | ||
| }, | ||
| { status: 404 } | ||
| ); | ||
| } | ||
| } satisfies ExportedHandler<Env>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "extends": "agents/tsconfig" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| { | ||
| "$schema": "../../node_modules/wrangler/config-schema.json", | ||
| "name": "dynamic-think-workflows-example", | ||
| "main": "src/index.ts", | ||
| "compatibility_date": "2026-06-11", | ||
| "compatibility_flags": ["nodejs_compat"], | ||
| "ai": { "binding": "AI", "remote": true }, | ||
| "worker_loaders": [{ "binding": "LOADER" }], | ||
| "durable_objects": { | ||
| "bindings": [ | ||
| { | ||
| "name": "MyAgent", | ||
| "class_name": "MyAgent" | ||
| } | ||
| ] | ||
| }, | ||
| "workflows": [ | ||
| { | ||
| "name": "dynamic-think", | ||
| "binding": "DYNAMIC_THINK_WF", | ||
| "class_name": "DynamicThinkWorkflow" | ||
| } | ||
| ], | ||
| "migrations": [ | ||
| { | ||
| "tag": "v1", | ||
| "new_sqlite_classes": ["MyAgent"] | ||
| } | ||
| ], | ||
| "observability": { | ||
| "enabled": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { DynamicThinkWorkflow } from "./loader"; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||
| createDynamicWorkflowEntrypoint, | ||||||||||||||||||||||||||||
| type WorkflowRunner | ||||||||||||||||||||||||||||
| } from "@cloudflare/dynamic-workflows"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| type DynamicThinkWorkflowMeta = { | ||||||||||||||||||||||||||||
| wfId: string; | ||||||||||||||||||||||||||||
| agentBinding: string; | ||||||||||||||||||||||||||||
| agentName: string; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| type DynamicThinkWorkflowEnv = { | ||||||||||||||||||||||||||||
| LOADER: WorkerLoader; | ||||||||||||||||||||||||||||
| [key: string]: unknown; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| type AgentStub = { | ||||||||||||||||||||||||||||
| getWorkflowCode(wfId: string): Promise<string>; | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * DynamicThinkWorkflow — register this as the `class_name` in your | ||||||||||||||||||||||||||||
| * `[[workflows]]` wrangler binding. When the engine calls `run()`, it | ||||||||||||||||||||||||||||
| * loads the generated ThinkWorkflow code from the agent via RPC, bundles | ||||||||||||||||||||||||||||
| * it with its npm dependencies (ThinkWorkflow, zod, etc.) via worker-bundler, | ||||||||||||||||||||||||||||
| * loads it as a Dynamic Worker, and dispatches execution to it. | ||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||
| * The generated code runs as a real ThinkWorkflow — `step.prompt()`, | ||||||||||||||||||||||||||||
| * `this.agent`, `step.do()`, `step.waitForEvent()` all work natively. | ||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||
| * ```ts | ||||||||||||||||||||||||||||
| * // wrangler.jsonc | ||||||||||||||||||||||||||||
| * { | ||||||||||||||||||||||||||||
| * "workflows": [{ | ||||||||||||||||||||||||||||
| * "name": "dynamic-think", | ||||||||||||||||||||||||||||
| * "binding": "DYNAMIC_THINK_WF", | ||||||||||||||||||||||||||||
| * "class_name": "DynamicThinkWorkflow" | ||||||||||||||||||||||||||||
| * }] | ||||||||||||||||||||||||||||
| * } | ||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||
| * // server.ts | ||||||||||||||||||||||||||||
| * export { DynamicThinkWorkflow } from "@cloudflare/think/dynamic-workflows"; | ||||||||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| export const DynamicThinkWorkflow = | ||||||||||||||||||||||||||||
| createDynamicWorkflowEntrypoint<DynamicThinkWorkflowEnv>( | ||||||||||||||||||||||||||||
| async ({ env, metadata }) => { | ||||||||||||||||||||||||||||
| const { createWorker } = await import("@cloudflare/worker-bundler"); | ||||||||||||||||||||||||||||
| const meta = metadata as unknown as DynamicThinkWorkflowMeta; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const agentNS = env[meta.agentBinding] as DurableObjectNamespace; | ||||||||||||||||||||||||||||
| const agentStub = agentNS.get( | ||||||||||||||||||||||||||||
| agentNS.idFromName(meta.agentName) | ||||||||||||||||||||||||||||
| ) as unknown as AgentStub; | ||||||||||||||||||||||||||||
| const code = await agentStub.getWorkflowCode(meta.wfId); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const { mainModule, modules } = await createWorker({ | ||||||||||||||||||||||||||||
| files: { | ||||||||||||||||||||||||||||
| "workflow.ts": code, | ||||||||||||||||||||||||||||
| "package.json": JSON.stringify({ | ||||||||||||||||||||||||||||
| dependencies: { | ||||||||||||||||||||||||||||
| "@cloudflare/think": "*", | ||||||||||||||||||||||||||||
| zod: "*" | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const worker = env.LOADER.get(`dwt-${meta.wfId}`, async () => ({ | ||||||||||||||||||||||||||||
| mainModule, | ||||||||||||||||||||||||||||
| modules, | ||||||||||||||||||||||||||||
| compatibilityDate: "2026-01-01", | ||||||||||||||||||||||||||||
| env: { [meta.agentBinding]: env[meta.agentBinding] } | ||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Dynamic worker missing The dynamic worker created in the loader at
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Dynamic worker env only forwards the agent binding — other bindings are unavailable At Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return worker.getEntrypoint( | ||||||||||||||||||||||||||||
| "GeneratedWorkflow" | ||||||||||||||||||||||||||||
| ) as unknown as WorkflowRunner; | ||||||||||||||||||||||||||||
|
Comment on lines
+115
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Hardcoded entrypoint name The loader at Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 Bundler package.json missing
agentsdependency used by generated codeThe loader's hardcoded
package.jsonatpackages/think/src/dynamic-workflows/loader.ts:61-66only lists@cloudflare/thinkandzodas dependencies. However, the example's generated code (examples/dynamic-think-workflows/src/index.ts:54) importsimport type { AgentWorkflowEvent } from "agents/workflows". Whileimport typeis erased at runtime, if any generated code does a value import fromagents, the bundler would fail to resolve it. Sinceagentsis a peer dependency of@cloudflare/think, whether it's transitively available depends on how@cloudflare/worker-bundlerresolves dependencies. Users generating code with runtime imports fromagentswould hit bundling errors.Was this helpful? React with 👍 or 👎 to provide feedback.