Important
PREVIEW ONLY This package is provided as a preview for feedback only. APIs are unstable and the design is subject to change.
Suitable for experiments, exploration and prototypes. It is NOT suitable for production use at this time.
A small end-to-end example that uses @cloudflare/think and
agents/workflows to triage a GitHub issue. The agent
runs inside a Cloudflare Container holding a
@cloudflare/workspace VFS; the workflow drives it
through one explore step (full toolset — the model decides whether
to attempt a fix or just gather findings), one tool-less structure
step that coerces the explore output into a zod schema, and a
terminal step that POSTs a unified diff back to the user.
client (./triage) worker container
│ POST /issue │ │
├──────────────────────────────▶ │ setContext + runWorkflow │
│ ├──── TRIAGE_WORKFLOW ────────────────────▶ │
│ │ fetch-issue │
│ │ → set-phase:explore │
│ │ → explore (tools, agentic loop) ──▶ wsd
│ POST /webhook (progress) │ │
│ ◀─────────────────────────── │ │
│ │ → set-phase:structure │
│ │ → structure (step.prompt, no tools) │
│ │ → build-patch (git diff via vfs) │
│ POST /webhook (DONE + patch) │ → notify-done │
│ ◀─────────────────────────── │ │
The worker exposes one route, POST /issue, with body
{ issue_url, webhook_url }. It derives a stable DO name from the
issue, binds the webhook into the agent, and starts the workflow. The
CLI runs that webhook server on 0.0.0.0 so a containerised worker can
reach it back over the host network.
The agent has two phases. The workflow flips between them via
agent.setPhase("explore" | "structure") inside a step.do before
each model step, so a replay after a crash still ends up in the
right phase. explore has the full toolset; structure has none —
the model can't be tempted to keep calling tools when its job is to
just emit JSON.
| Tool | Source |
|---|---|
read |
vendored from hackspace/fs-tools |
ls |
src/agent.ts |
write |
vendored from hackspace/fs-tools |
edit |
vendored from hackspace/fs-tools |
exec |
src/tools/exec.ts |
report_update |
src/tools/report-update.ts |
share |
src/tools/share.ts (optional) |
The share tool uploads a workspace file to R2 and returns a
time-limited link, so the agent can hand the user an artifact it
produced. The worker backend shell also exposes
assets publish <path> [<expiry>], which prints the same kind of
link to stdout from exec. Both are registered only when the R2
credentials below are set; without them the agent runs unchanged and
the share surfaces are omitted. See
docs/14_assets_interface.md.
exec is wired to a Workspace with two backends: a "shell"
backend (just-bash in a Dynamic Worker through env.LOADER) and
a "container" backend (Cloudflare Container running wsd). The
shell backend is the default; the tool description tells the model
which backend each kind of command belongs on, and the model is
expected to try the cheap shell first and route to the container
for anything the shell can't run (npm install, language-specific
tooling, ...). See
docs/05_shell_interface.md
for the backend selection model and the cross-backend write
caveat.
Git lives on the shell backend. The worker backend's just-bash
shell registers a built-in git command (see
docs/13_git_interface.md) that
forwards every invocation across the loopback to the host's
workspace.git.cli(...). That means git clone, git status,
git diff, and friends run from inside exec({ command: "git ...", backend: "shell" }) even though the shell isolate has
globalOutbound: null — the actual HTTP request happens on the
host durable object, not in the isolate. Only https:// URLs are
supported; ssh:// and git:// are rejected up front. The clone
runs at depth=1 by default; pass --depth=<N> for more history.
The workflow itself reaches for the typed git API directly when
it needs a diff for the terminal webhook payload — see
TriageAgent.gitDiff in src/agent.ts, which calls
workspace.git.diff rather than going through exec. That skips
a shell round trip and gives the workflow the diff text in-band.
The DO mounts an R2 bucket (R2_SKILLS) at /workspace/.agents via
R2Bucket(env.R2_SKILLS, { prefix: ".agents/" }). On the first call
into the workspace the indexer pages through the bucket and streams
every object into vfs_nodes; from then on the agent sees
/workspace/.agents/skills/triage/SKILL.md like any other file. The
mount is read-only — writes under /workspace/.agents reject with
EROFS.
The system prompt tells the model to read that file before it starts, so the playbook (clone, decide bet, fix or write findings, report progress, summary contract) is delivered in-band instead of being baked into the prompt.
Seed the bucket once with the bundled fixture before running:
# Local miniflare bucket — use this with `wrangler dev`.
npm run seed:r2:local --workspace @cloudflare/example-think
# Real Cloudflare R2 bucket — use this after `wrangler deploy`.
npm run seed:r2 --workspace @cloudflare/example-thinkThe fixture lives at
examples/think/seed/r2-skills/.agents/skills/triage/SKILL.md; edit
it and re-run the seed script to update the bucket. The DO picks the
new bytes up on the next session (eager mount, indexed once per
workspace lifetime).
Requires Docker. The Dockerfile pulls
ghcr.io/cloudflare/workspace-wsd-linux-x64:<version> from the
public GitHub Container Registry on first build; no local image
prep is needed.
# From the repo root:
npm install
# Two terminals — worker on one, CLI on the other.
cd examples/think
npm run dev # terminal 1
./cli/triage.mjs https://github.com/owner/repo/issues/42 # terminal 2The CLI prints progress lines as they arrive, then the final commit
subject/body and a colourised unified diff. It exits 0 when the
worker emits a message ending in DONE.
Useful flags:
--worker URL— point the CLI at a different worker base URL (alsoTRIAGE_WORKER). Defaulthttp://127.0.0.1:8787.--host HOST— host name the worker should call back on (alsoTRIAGE_HOST). Defaults to the first non-loopback IPv4. Override this if you're running the worker inside a container or VM and the auto-pick is wrong for that network.
The worker is configured in wrangler.jsonc:
-
AI— Workers AI binding (model:@cf/moonshotai/kimi-k2.6). -
LOADER— Worker Loader binding. The TriageAgent's Workspace uses it to mint the Dynamic Worker that hosts the"shell"backend. -
TriageAgent— container-enabled DO that owns one Workspace + one Think agent per issue. The Workspace carries two backends: the worker backend throughenv.LOADERand the container backend throughthis.ctx.container. -
TRIAGE_WORKFLOW— workflow binding pointing atTriageWorkflow. -
ASSETS— R2 bucket thesharetool uploads to. Create it once before deploying:wrangler r2 bucket create think-example-assets
The share tool also needs R2 S3 credentials to presign URLs — the
bucket binding alone can't mint them. Create an R2 API token scoped
to the bucket and set the values as secrets:
wrangler secret put R2_ACCESS_KEY_ID
wrangler secret put R2_SECRET_ACCESS_KEY
wrangler secret put CLOUDFLARE_ACCOUNT_IDR2_ENDPOINT can be set instead of CLOUDFLARE_ACCOUNT_ID when
using a custom S3-compatible endpoint.
Without these the worker still runs; the share tool is not offered
to the model, and assets publish is not configured in the shell.
ARTIFACTS— optional Cloudflare Artifacts binding, commented out by default. Uncomment theartifactsstanza inwrangler.jsoncand re-runwrangler typesto enable it.TriageAgentthen builds a session-scoped artifacts client so every repository it creates is isolated under the agent's own durable object id. The worker backend exposes the same client as an in-shellartifactscommand, and the workflow's final step imports the source repository into Artifacts, pushes the agent's changes to a review branch, and includes the Artifacts remote and branch in the terminal webhook message. When the binding is absent, the demo runs unchanged and skips that publication step. Seedocs/15_artifacts_interface.md.
No GitHub auth. The issue must be on a public repository.
wrangler deploy works against any account that has Workers AI,
Workflows, and Cloudflare Containers enabled. After deploy, the CLI's
webhook URL must be reachable from your worker — use a tunnel
(cloudflared tunnel, ngrok http, etc.) and pass --host so the
CLI advertises the tunnel hostname instead of its local IP.
- Private repos / GitHub auth.
- Write back to the issue (comments, labels). The patch is returned to the CLI; applying it is the user's problem.
- Persistence of agent state beyond what Think and Workflows give us for free.
- Streaming exec output to a UI; the agent runs each
execto completion in one tool round.