Related issue
#1752 — First-class detached sub-agent runs with a durable completion hook
overlaps on the "don't pollute main history" motivation, but that issue is about async A2A delegation
within an agent's own turn. This request is about calling a Think agent from a Cloudflare Workflow
step — with the full agentic loop, structured output, and zero conversation pollution — without having
to build a second nested ThinkWorkflow as a workaround.
What we want
We want an agent workflow step that works exactly like the llm step — a clean, isolated call that
returns a result — except instead of a single raw streamText call it runs the Think agent's full
agentic capabilities: its configured model, its system prompt, its tools, its memory, and the full
inference loop.
| Need |
llm step today |
agent step (desired) |
| Agent's configured model |
uses caller-supplied model |
✅ uses the agent's getModel() |
| System prompt / soul |
❌ caller writes the prompt |
✅ agent's own getSystemPrompt() |
| Tool use + agentic loop |
❌ single inference call |
✅ multi-turn, tools fire and retry |
| Stateful memory |
❌ |
✅ agent's SQLite memory blocks |
| Structured output (Zod schema) |
✅ |
✅ same final_answer enforcement |
| Isolated — no chat history write |
✅ never touches history |
✅ (desired — currently broken) |
| Multiple concurrent workflows |
✅ |
✅ (desired — currently broken) |
In short: same ergonomics as llm, same power as a full Think agent.
Problem
There is currently no Think primitive that satisfies all of the above.
step.prompt() gets us schema enforcement and the agentic loop but writes every run to the agent's
main cf_agent_messages history. An agent used as a workflow step accumulates every automated
invocation in its chat — mixing operational calls with user-facing conversation. Two concurrent
workflows hitting the same agent DO serialize and interleave in the same history.
Current workaround — and why it is painful
Because no isolated primitive exists, the only way to get the full agentic loop with structured output
from a WorkflowEntrypoint step is to build a second durable workflow inside the Think agent itself:
- The outer
WorkflowEntrypoint calls a @callable on the Think agent DO, then immediately calls
step.waitForEvent to block.
- That
@callable spawns a ThinkWorkflow (a nested Cloudflare Workflow bound to the agent DO).
- The inner
ThinkWorkflow runs step.prompt() to get the full agentic loop and schema enforcement.
- When the inner workflow finishes, it calls
sendWorkflowEvent back to the outer
WorkflowEntrypoint with the result.
- The outer workflow wakes up, reads the event payload, and continues.
WorkflowEntrypoint (outer)
│
├─ step.do("agent:start")
│ └─ agent.startPrompt(input, schema, parentRef) // @callable — fire and return
│ └─ agent.runWorkflow("AGENT_TASK_WORKFLOW") // spawns a second durable workflow
│
└─ step.waitForEvent("agent:wait", { timeout: "1 hour" })
│
└─ ThinkWorkflow (inner, nested inside agent DO)
└─ step.prompt("respond", { output: schema })
└─ full agent turn: tools, loop, final_answer
└─ sendWorkflowEvent(parentRef) // delivers result back to outer workflow
What this workaround costs
-
Two nested durable workflows for every single agent step.
The outer WorkflowEntrypoint's step.do already provides checkpointing and replay — the inner
ThinkWorkflow adds a second layer of Cloudflare Workflow overhead that solves a problem the
outer layer already solves.
-
History still polluted. step.prompt() inside the inner ThinkWorkflow calls
submitMessages(), which appends the turn to cf_agent_messages. Every workflow run leaves a
trace in the agent's chat history. Multiple concurrent workflow runs hitting the same agent
serialize at the DO and cross-contaminate each other's context.
-
Boilerplate every consumer has to rediscover. The bridge from WorkflowEntrypoint → Think
agent → result requires wiring a @callable, a nested ThinkWorkflow, and a
sendWorkflowEvent / waitForEvent handshake. There is no SDK primitive that does this — every
team that wants this pattern has to build and maintain it themselves.
Proposed API
// On the Think class — callable over RPC from any WorkflowEntrypoint
async runTask<Schema extends ZodObject>(
input: string,
options: {
output: Schema // enforced via synthetic final_answer tool (same as step.prompt)
signal?: AbortSignal
}
): Promise<z.infer<Schema>>
Internally it would:
- Run inference in a child facet (same isolation
startAgentToolRun already provides —
separate from cf_agent_messages, no history writes)
- Enforce the output schema via the synthetic
final_answer tool already used by step.prompt()
- Return the validated result synchronously to the caller
- Require no nested
ThinkWorkflow — durability is owned by the caller's step.do
What the agent step becomes
// With runTask() — single step.do, isolated, no nested workflow
const result = await step.do(label, { retries: { limit: 2 }, timeout: "15 minutes" }, async () => {
const stub = await getAgentByName(env.UserAgent, agentId)
return stub.runTask(input, { output: schema })
})
Additional benefits:
- Any number of concurrent workflows can target the same agent without serializing or
cross-contaminating history — each runTask call runs in its own child facet.
retries: is safe — step.do replay re-calls runTask, starting a fresh child-facet run
rather than appending a duplicate turn to main history (the current stub.prompt() retry hazard).
Environment
@cloudflare/think (latest)
- Cloudflare Workers + Durable Objects
- Cloudflare Workflows (
WorkflowEntrypoint)
Related issue
#1752 — First-class detached sub-agent runs with a durable completion hook
overlaps on the "don't pollute main history" motivation, but that issue is about async A2A delegation
within an agent's own turn. This request is about calling a Think agent from a Cloudflare Workflow
step — with the full agentic loop, structured output, and zero conversation pollution — without having
to build a second nested
ThinkWorkflowas a workaround.What we want
We want an
agentworkflow step that works exactly like thellmstep — a clean, isolated call thatreturns a result — except instead of a single raw
streamTextcall it runs the Think agent's fullagentic capabilities: its configured model, its system prompt, its tools, its memory, and the full
inference loop.
llmstep todayagentstep (desired)getModel()getSystemPrompt()final_answerenforcementIn short: same ergonomics as
llm, same power as a full Think agent.Problem
There is currently no Think primitive that satisfies all of the above.
step.prompt()gets us schema enforcement and the agentic loop but writes every run to the agent'smain
cf_agent_messageshistory. An agent used as a workflow step accumulates every automatedinvocation in its chat — mixing operational calls with user-facing conversation. Two concurrent
workflows hitting the same agent DO serialize and interleave in the same history.
Current workaround — and why it is painful
Because no isolated primitive exists, the only way to get the full agentic loop with structured output
from a
WorkflowEntrypointstep is to build a second durable workflow inside the Think agent itself:WorkflowEntrypointcalls a@callableon the Think agent DO, then immediately callsstep.waitForEventto block.@callablespawns aThinkWorkflow(a nested Cloudflare Workflow bound to the agent DO).ThinkWorkflowrunsstep.prompt()to get the full agentic loop and schema enforcement.sendWorkflowEventback to the outerWorkflowEntrypointwith the result.What this workaround costs
Two nested durable workflows for every single agent step.
The outer
WorkflowEntrypoint'sstep.doalready provides checkpointing and replay — the innerThinkWorkflowadds a second layer of Cloudflare Workflow overhead that solves a problem theouter layer already solves.
History still polluted.
step.prompt()inside the innerThinkWorkflowcallssubmitMessages(), which appends the turn tocf_agent_messages. Every workflow run leaves atrace in the agent's chat history. Multiple concurrent workflow runs hitting the same agent
serialize at the DO and cross-contaminate each other's context.
Boilerplate every consumer has to rediscover. The bridge from
WorkflowEntrypoint→ Thinkagent → result requires wiring a
@callable, a nestedThinkWorkflow, and asendWorkflowEvent/waitForEventhandshake. There is no SDK primitive that does this — everyteam that wants this pattern has to build and maintain it themselves.
Proposed API
Internally it would:
startAgentToolRunalready provides —separate from
cf_agent_messages, no history writes)final_answertool already used bystep.prompt()ThinkWorkflow— durability is owned by the caller'sstep.doWhat the agent step becomes
Additional benefits:
cross-contaminating history — each
runTaskcall runs in its own child facet.retries:is safe —step.doreplay re-callsrunTask, starting a fresh child-facet runrather than appending a duplicate turn to main history (the current
stub.prompt()retry hazard).Environment
@cloudflare/think(latest)WorkflowEntrypoint)