Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/dynamic-think-workflows.md
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)` — internal 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).
57 changes: 57 additions & 0 deletions docs/think/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,60 @@ private outbox because it needs to store an event until delivery succeeds.

Use Workflows when the process has multiple deterministic steps, long waits, or
human approval.

## Dynamic Workflows

When the workflow body itself is generated at runtime (for example, by an LLM),
use `runDynamicWorkflow()` instead of registering a static `ThinkWorkflow`
subclass. It stores the generated TypeScript source, then starts a Workflow
instance whose code is bundled and executed as a Dynamic Worker on first run.

Register the `DynamicThinkWorkflow` entrypoint as the `class_name` of a
`workflows` binding in `wrangler.jsonc`:

```typescript
// server.ts
export { DynamicThinkWorkflow } from "@cloudflare/think/dynamic-workflows";
```

Then call `runDynamicWorkflow()` from your Think agent:

```typescript
const workflowId = await this.runDynamicWorkflow(
"DYNAMIC_THINK_WF", // the workflow binding name
generatedCode, // TypeScript source (see the class-name requirement below)
{ topic: "release notes" } // params forwarded to the workflow's run()
);
```

The generated code must declare a class named `GeneratedWorkflow` that extends
`ThinkWorkflow` — this is the entrypoint the loader dispatches to:

```typescript
export default class GeneratedWorkflow extends ThinkWorkflow {
async run(event, step) {
/* ... */
}
}
```

The generated code runs as a real `ThinkWorkflow`, so `step.prompt()`,
`step.do()`, `step.sleep()`, and `step.waitForEvent()` all work natively.

Notes:

- Generated code is validated by `validateWorkflowCode()` before it is stored.
The default implementation enforces a non-empty body, a size limit
(`Think.MAX_DYNAMIC_WORKFLOW_CODE_BYTES`), and that the code declares a
`GeneratedWorkflow` class. Override it in a subclass to add stricter checks.
You remain responsible for trusting the source of any LLM-generated code you
pass in.
- Passing an explicit `options.id` that is already tracked throws — the code is
not stored, so duplicate IDs do not orphan rows.
- `_getWorkflowCode()` is reachable via Durable Object service-binding RPC (it
is excluded from the WebSocket callable surface, but the leading underscore
is a naming convention, not an access-control boundary). Treat stored code as
sensitive.
- Stored code is retained for the lifetime of the agent's Durable Object so
long-running workflows can be resumed. It is not evicted automatically.
- See the `examples/dynamic-think-workflows` example for a complete setup.
31 changes: 31 additions & 0 deletions examples/dynamic-think-workflows/README.md
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
24 changes: 24 additions & 0 deletions examples/dynamic-think-workflows/package.json
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"
}
}
115 changes: 115 additions & 0 deletions examples/dynamic-think-workflows/src/index.ts
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>;
3 changes: 3 additions & 0 deletions examples/dynamic-think-workflows/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "agents/tsconfig"
}
33 changes: 33 additions & 0 deletions examples/dynamic-think-workflows/wrangler.jsonc
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
}
}
40 changes: 25 additions & 15 deletions packages/think/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"dependencies": {
"@cloudflare/codemode": ">=0.4.0",
"@cloudflare/dynamic-workflows": ">=0.1.1",
"@cloudflare/shell": ">=0.4.0",
"aywson": "^0.0.16",
"chat": "^4.30.0",
Expand All @@ -33,13 +34,33 @@
"smol-toml": "^1.6.1",
"yargs": "^18.0.0"
},
"peerDependencies": {
"@chat-adapter/telegram": "^4.29.0",
"@cloudflare/worker-bundler": ">=0.2.1",
"agents": ">=0.16.0 <1.0.0",
"ai": "^6.0.182",
"vite": ">=6 <9",
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"@chat-adapter/telegram": {
"optional": true
},
"@cloudflare/worker-bundler": {
"optional": true
},
"vite": {
"optional": true
}
},
"devDependencies": {
"@ai-sdk/anthropic": "^3.0.83",
"@ai-sdk/openai": "^3.0.70",
"@ai-sdk/react": "^3.0.204",
"@chat-adapter/telegram": "^4.30.0",
"@cloudflare/ai-chat": "workspace:*",
"@cloudflare/kumo": "^2.5.2",
"@cloudflare/worker-bundler": "workspace:*",
"@phosphor-icons/react": "^2.1.10",
"@streamdown/code": "^1.1.1",
"@tailwindcss/vite": "^4",
Expand All @@ -58,21 +79,6 @@
"vite": "^8.0.16",
"zod": "^4.4.3"
},
"peerDependencies": {
"@chat-adapter/telegram": "^4.29.0",
"agents": ">=0.16.0 <1.0.0",
"ai": "^6.0.182",
"vite": ">=6 <9",
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"@chat-adapter/telegram": {
"optional": true
},
"vite": {
"optional": true
}
},
"exports": {
".": {
"types": "./dist/think.d.ts",
Expand All @@ -86,6 +92,10 @@
"types": "./dist/workflows.d.ts",
"import": "./dist/workflows.js"
},
"./dynamic-workflows": {
"types": "./dist/dynamic-workflows/index.d.ts",
"import": "./dist/dynamic-workflows/index.js"
},
"./framework": {
"types": "./dist/framework/index.d.ts",
"import": "./dist/framework/index.js"
Expand Down
1 change: 1 addition & 0 deletions packages/think/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async function main() {
entry: [
"src/think.ts",
"src/workflows.ts",
"src/dynamic-workflows/index.ts",
"src/extensions/index.ts",
"src/framework/index.ts",
"src/server-entry.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/think/src/dynamic-workflows/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DynamicThinkWorkflow } from "./loader";
Loading
Loading