config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config

gitconfig: name/email from chezmoi data, gh credential helper by PATH
lookup (portable to darwin). claude: no credentials, history, projects,
sessions, or plugin caches. ssh config as private_ (0600).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Prad NukalaandClaude Fable 5 committed 2026-08-09 14:53:46 -04:00
1 parent 3c691e8dc5
commit 20891f3910
476 files changed
+59086 -7

No files matched your search

+7
View File
@@ -12,3 +12,10 @@
{"id":"int-68e58a83","kind":"field_change","created_at":"2026-08-09T18:52:20.158250426Z","actor":"Prad Nukala","issue_id":"chezmoi-xi5","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} {"id":"int-68e58a83","kind":"field_change","created_at":"2026-08-09T18:52:20.158250426Z","actor":"Prad Nukala","issue_id":"chezmoi-xi5","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-7b4d4c0d","kind":"field_change","created_at":"2026-08-09T18:52:41.13963999Z","actor":"Prad Nukala","issue_id":"chezmoi-xi5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"tmux.conf real content (was symlink_); no vertical geometry present; plugins unmanaged, tpm bootstrap in post script"}} {"id":"int-7b4d4c0d","kind":"field_change","created_at":"2026-08-09T18:52:41.13963999Z","actor":"Prad Nukala","issue_id":"chezmoi-xi5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"tmux.conf real content (was symlink_); no vertical geometry present; plugins unmanaged, tpm bootstrap in post script"}}
{"id":"int-e52df1d7","kind":"field_change","created_at":"2026-08-09T18:52:41.350493498Z","actor":"Prad Nukala","issue_id":"chezmoi-utl","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} {"id":"int-e52df1d7","kind":"field_change","created_at":"2026-08-09T18:52:41.350493498Z","actor":"Prad Nukala","issue_id":"chezmoi-utl","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-9f6276b6","kind":"field_change","created_at":"2026-08-09T18:53:14.172501264Z","actor":"Prad Nukala","issue_id":"chezmoi-utl","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
{"id":"int-99fddaae","kind":"field_change","created_at":"2026-08-09T18:53:14.324562287Z","actor":"Prad Nukala","issue_id":"chezmoi-hou","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
{"id":"int-5e972502","kind":"field_change","created_at":"2026-08-09T18:53:14.477632454Z","actor":"Prad Nukala","issue_id":"chezmoi-x7f","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
{"id":"int-2eaa43fb","kind":"field_change","created_at":"2026-08-09T18:53:14.636752198Z","actor":"Prad Nukala","issue_id":"chezmoi-4sj","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
{"id":"int-ec238d3e","kind":"field_change","created_at":"2026-08-09T18:53:14.795389276Z","actor":"Prad Nukala","issue_id":"chezmoi-21v","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
{"id":"int-1e6b9af3","kind":"field_change","created_at":"2026-08-09T18:53:15.496566548Z","actor":"Prad Nukala","issue_id":"chezmoi-a2k","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
{"id":"int-bcfce2e0","kind":"field_change","created_at":"2026-08-09T18:53:15.63271081Z","actor":"Prad Nukala","issue_id":"chezmoi-exq","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
+7 -7
View File
@@ -6,14 +6,14 @@
{"_type":"issue","id":"chezmoi-9sg","title":"Arch layer: niri, noctalia, fontconfig, mimeapps, systemd user units — linux-gated","description":"niri config.d split preserved with exact_ dir; noctalia; fontconfig; mimeapps.list; systemd user units; foot/alacritty if live. All gated in .chezmoiignore.tmpl for darwin. niri validate must pass.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:17Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:17Z","dependencies":[{"issue_id":"chezmoi-9sg","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-9sg","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-9sg","title":"Arch layer: niri, noctalia, fontconfig, mimeapps, systemd user units — linux-gated","description":"niri config.d split preserved with exact_ dir; noctalia; fontconfig; mimeapps.list; systemd user units; foot/alacritty if live. All gated in .chezmoiignore.tmpl for darwin. niri validate must pass.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:17Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:17Z","dependencies":[{"issue_id":"chezmoi-9sg","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-9sg","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-iu2","title":"packages.yaml: translate dotstate manifests + live state into declarative manifest","description":"Curate Noctalia pacman list (drop base-system noise), custom installers, cargo/go tools, shared gh_extensions; darwin brew/cask section. Install scripts run_onchange keyed on yaml hash: pacman --needed + paru | brew loop; gh extension install loop; linux systemctl --user enable; post: fc-cache, bat cache.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:17Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:17Z","dependencies":[{"issue_id":"chezmoi-iu2","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:53Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-iu2","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:53Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-iu2","title":"packages.yaml: translate dotstate manifests + live state into declarative manifest","description":"Curate Noctalia pacman list (drop base-system noise), custom installers, cargo/go tools, shared gh_extensions; darwin brew/cask section. Install scripts run_onchange keyed on yaml hash: pacman --needed + paru | brew loop; gh extension install loop; linux systemctl --user enable; post: fc-cache, bat cache.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:17Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:17Z","dependencies":[{"issue_id":"chezmoi-iu2","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:53Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-iu2","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:53Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-udp","title":"Validation: darwin simulation","description":"chezmoi execute-template with os=darwin on every .tmpl; darwin-mode dry-run apply writes zero linux-gated paths, errors on nothing. Full Mac apply deferred until hardware available.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:17Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:17Z","dependencies":[{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-21v","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-4sj","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-9sg","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-a2k","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-exq","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-fde","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-hou","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-iu2","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-sd9","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-utl","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-uw2","type":"blocks","created_at":"2026-08-09T14:42:59Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-x7f","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-xi5","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":13,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"chezmoi-udp","title":"Validation: darwin simulation","description":"chezmoi execute-template with os=darwin on every .tmpl; darwin-mode dry-run apply writes zero linux-gated paths, errors on nothing. Full Mac apply deferred until hardware available.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:17Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:17Z","dependencies":[{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-21v","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-4sj","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-9sg","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-a2k","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-exq","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-fde","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-hou","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-iu2","type":"blocks","created_at":"2026-08-09T14:43:01Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-sd9","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-utl","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-uw2","type":"blocks","created_at":"2026-08-09T14:42:59Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-x7f","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-udp","depends_on_id":"chezmoi-xi5","type":"blocks","created_at":"2026-08-09T14:43:00Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":13,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"chezmoi-21v","title":"gh: config file + declarative extension list","description":"~/.config/gh config.yml (hosts.yml has oauth token - exclude/encrypt). Extensions: clean-branches, commit, dash, notify into packages.yaml shared.gh_extensions.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:06Z","dependencies":[{"issue_id":"chezmoi-21v","depends_on_id":"chezmoi-1a0","type":"blocks","created_at":"2026-08-09T14:42:24Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-21v","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-21v","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-21v","title":"gh: config file + declarative extension list","description":"~/.config/gh config.yml (hosts.yml has oauth token - exclude/encrypt). Extensions: clean-branches, commit, dash, notify into packages.yaml shared.gh_extensions.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:53:15Z","closed_at":"2026-08-09T18:53:15Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-21v","depends_on_id":"chezmoi-1a0","type":"blocks","created_at":"2026-08-09T14:42:24Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-21v","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-21v","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-4sj","title":"lazydocker: static config from live","description":"~/.config/lazydocker (not in dotstate).","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:06Z","dependencies":[{"issue_id":"chezmoi-4sj","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-4sj","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-4sj","title":"lazydocker: static config from live","description":"~/.config/lazydocker (not in dotstate).","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:53:15Z","closed_at":"2026-08-09T18:53:15Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-4sj","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-4sj","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-a2k","title":"claude: settings, commands, agents, hooks, skills only","description":"~/.claude: settings.json, commands/, hooks/, skills/, CLAUDE.md. Never credentials/history/projects/sessions.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:06Z","dependencies":[{"issue_id":"chezmoi-a2k","depends_on_id":"chezmoi-1a0","type":"blocks","created_at":"2026-08-09T14:42:24Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-a2k","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-a2k","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-a2k","title":"claude: settings, commands, agents, hooks, skills only","description":"~/.claude: settings.json, commands/, hooks/, skills/, CLAUDE.md. Never credentials/history/projects/sessions.","status":"in_progress","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:53:15Z","started_at":"2026-08-09T18:53:15Z","dependencies":[{"issue_id":"chezmoi-a2k","depends_on_id":"chezmoi-1a0","type":"blocks","created_at":"2026-08-09T14:42:24Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-a2k","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-a2k","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-exq","title":"git: dot_gitconfig.tmpl with templated email/signing key","description":"~/.gitconfig from live; email + signing key from chezmoi data.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:06Z","dependencies":[{"issue_id":"chezmoi-exq","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-exq","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-exq","title":"git: dot_gitconfig.tmpl with templated email/signing key","description":"~/.gitconfig from live; email + signing key from chezmoi data.","status":"in_progress","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:53:16Z","started_at":"2026-08-09T18:53:16Z","dependencies":[{"issue_id":"chezmoi-exq","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-exq","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-fde","title":"starship: prompt config, universal","description":"Keep starship (user decision — omp dropped). dot_config/starship.toml from live; starship in packages.yaml both OSes. Noctalia palette block is auto-generated on linux — noctalia rewrites it in place; acceptable drift, noted.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:51:26Z","closed_at":"2026-08-09T18:51:26Z","close_reason":"starship.toml added from live; packages updated","dependencies":[{"issue_id":"chezmoi-fde","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-fde","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-fde","title":"starship: prompt config, universal","description":"Keep starship (user decision — omp dropped). dot_config/starship.toml from live; starship in packages.yaml both OSes. Noctalia palette block is auto-generated on linux — noctalia rewrites it in place; acceptable drift, noted.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:51:26Z","closed_at":"2026-08-09T18:51:26Z","close_reason":"starship.toml added from live; packages updated","dependencies":[{"issue_id":"chezmoi-fde","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:52Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-fde","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-x7f","title":"lazygit: static config","description":"~/.config/lazygit. Already partially in repo — reconcile.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:06Z","dependencies":[{"issue_id":"chezmoi-x7f","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-x7f","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-x7f","title":"lazygit: static config","description":"~/.config/lazygit. Already partially in repo — reconcile.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:06Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:53:14Z","closed_at":"2026-08-09T18:53:14Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-x7f","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:51Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-x7f","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-hou","title":"yazi: config, opener rules OS-branched","description":"~/.config/yazi. open vs xdg-open template branch.","status":"open","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:42:05Z","dependencies":[{"issue_id":"chezmoi-hou","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-hou","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-hou","title":"yazi: config, opener rules OS-branched","description":"~/.config/yazi. open vs xdg-open template branch.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:53:14Z","closed_at":"2026-08-09T18:53:14Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-hou","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-hou","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-sd9","title":"nvim: AstroNvim config, static","description":"chezmoi add ~/.config/nvim as-is, no templating. Exclude lazy-lock churn decision noted.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:52:20Z","started_at":"2026-08-09T18:51:54Z","closed_at":"2026-08-09T18:52:20Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-sd9","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-sd9","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-sd9","title":"nvim: AstroNvim config, static","description":"chezmoi add ~/.config/nvim as-is, no templating. Exclude lazy-lock churn decision noted.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:52:20Z","started_at":"2026-08-09T18:51:54Z","closed_at":"2026-08-09T18:52:20Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-sd9","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-sd9","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-utl","title":"kitty: live config, OS-branch fonts/paths if needed","description":"~/.config/kitty. Keybinds shared.","status":"in_progress","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:52:41Z","started_at":"2026-08-09T18:52:41Z","dependencies":[{"issue_id":"chezmoi-utl","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-utl","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-utl","title":"kitty: live config, OS-branch fonts/paths if needed","description":"~/.config/kitty. Keybinds shared.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:53:14Z","started_at":"2026-08-09T18:52:41Z","closed_at":"2026-08-09T18:53:14Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-utl","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-utl","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-uw2","title":"fish: config, conf.d, functions, completions, bindings","description":"Live ~/.config/fish (Rogz storage superset; Noctalia canonical, deltas to MIGRATION-NOTES). OS-branch clipboard/open/package paths. secrets.fish excluded (own issue). fish -n all files.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:51:54Z","started_at":"2026-08-09T18:49:28Z","closed_at":"2026-08-09T18:51:54Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-1a0","type":"blocks","created_at":"2026-08-09T14:42:24Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-uw2","title":"fish: config, conf.d, functions, completions, bindings","description":"Live ~/.config/fish (Rogz storage superset; Noctalia canonical, deltas to MIGRATION-NOTES). OS-branch clipboard/open/package paths. secrets.fish excluded (own issue). fish -n all files.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:51:54Z","started_at":"2026-08-09T18:49:28Z","closed_at":"2026-08-09T18:51:54Z","close_reason":"Closed","dependencies":[{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-1a0","type":"blocks","created_at":"2026-08-09T14:42:24Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-uw2","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:49Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"chezmoi-xi5","title":"tmux: config from live, strip vertical geometry","description":"~/.config/tmux (tmux.conf symlink from Vertical-Rogz_AppleStudio). Verify live, gate vertical-display-specific bits if present. Plugins dir decision.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:52:41Z","started_at":"2026-08-09T18:52:20Z","closed_at":"2026-08-09T18:52:41Z","close_reason":"tmux.conf real content (was symlink_); no vertical geometry present; plugins unmanaged, tpm bootstrap in post script","dependencies":[{"issue_id":"chezmoi-xi5","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-xi5","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"chezmoi-xi5","title":"tmux: config from live, strip vertical geometry","description":"~/.config/tmux (tmux.conf symlink from Vertical-Rogz_AppleStudio). Verify live, gate vertical-display-specific bits if present. Plugins dir decision.","status":"closed","priority":2,"issue_type":"task","owner":"prad@sonr.io","created_at":"2026-08-09T18:42:05Z","created_by":"Prad Nukala","updated_at":"2026-08-09T18:52:41Z","started_at":"2026-08-09T18:52:20Z","closed_at":"2026-08-09T18:52:41Z","close_reason":"tmux.conf real content (was symlink_); no vertical geometry present; plugins unmanaged, tpm bootstrap in post script","dependencies":[{"issue_id":"chezmoi-xi5","depends_on_id":"chezmoi-byc","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"},{"issue_id":"chezmoi-xi5","depends_on_id":"chezmoi-rnv","type":"blocks","created_at":"2026-08-09T14:42:50Z","created_by":"Prad Nukala","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0}
+118
View File
@@ -0,0 +1,118 @@
---
description: Recursively implement all pending beads issues until every one is closed with tests passing, builds succeeding, requirements met, and blockers cleared
argument-hint: [optional: priority filter e.g. "p0-p2" | epic id | "dry-run"]
allowed-tools: Bash(bd:*), Bash(git:*), Bash(go:*), Bash(pnpm:*), Bash(cargo:*), Bash(make:*), Read, Write, Edit, Glob, Grep, Task
---
# /bd-drain — Recursive Beads Completion Workflow
Drain the beads backlog to zero. Work every ready issue to full completion, unblock dependent issues as they clear, and repeat until no open, actionable work remains. Arguments: `$ARGUMENTS` (optional priority filter like `p0-p2`, an epic ID to scope to, or `dry-run` to plan without executing).
## Phase 0 — Session Bootstrap
1. Run `bd dolt pull` to sync beads state from remote.
2. Run `bd prime` if context was compacted or this is a fresh session.
3. Run `bd doctor` — if sync problems or missing hooks are reported, fix them before proceeding.
4. Run `bd stats` and record the baseline: open / in_progress / blocked counts. This is the burn-down target.
5. Run `bd memories drain` and `bd memories <project keyword>` to load any prior insights about this workflow or codebase gotchas.
6. Detect the project's verification commands by inspecting the repo (Makefile, Taskfile, turbo.json, package.json scripts, go.mod, Cargo.toml). Record:
- **TEST_CMD** (e.g. `go test ./...`, `pnpm turbo test`, `cargo test`)
- **BUILD_CMD** (e.g. `go build ./...`, `pnpm turbo build`)
- **LINT_CMD** if present (e.g. `golangci-lint run`, `pnpm lint`)
7. Run TEST_CMD and BUILD_CMD once **before touching anything** to establish a green baseline. If the baseline is already red, create a P0 bug with `bd create` describing the pre-existing failure and fix it first — never build on a broken base.
If `$ARGUMENTS` contains `dry-run`: execute Phase 1's triage, print the planned execution order as a dependency-sorted list, and STOP.
## Phase 1 — Triage & Ordering
1. Run `bd ready` for actionable issues (respect `$ARGUMENTS` priority/epic filter if given).
2. Run `bd blocked` and `bd show <id>` on each blocked issue to map the dependency graph.
3. Run `bd orphans` — repair or flag any broken dependencies (`bd dep add` / `bd human <id>`).
4. Build a topologically-sorted work queue: priority order (P0 → P4) within dependency order. Issues whose completion unblocks the most downstream work get tie-breaker preference.
5. Run `bd lint` on the queue. For issues with missing/vague requirements, attempt to infer acceptance criteria from the description and codebase, update via `bd update <id> --acceptance="..."`. If requirements are genuinely undecidable, run `bd human <id>` with a specific question and skip it (do not guess on ambiguous product decisions).
## Phase 2 — The Drain Loop
Repeat until the exit condition in Phase 3 is met. For **each** issue in the queue:
### 2a. Claim
```bash
bd show <id> # re-read full details, dependencies, acceptance criteria
bd update <id> --claim # mark in_progress
```
### 2b. Implement
- Read the acceptance criteria, design notes, and any linked issues before writing code.
- Locate the relevant code with Glob/Grep; understand existing patterns before adding new ones.
- Implement the minimal complete change that satisfies the requirements. Follow existing repo conventions.
- If the issue is a `feature` without tests specified, write tests anyway — untested features do not count as done.
- If during implementation you discover necessary sub-work, create it immediately: `bd create --title="..." --description="..." --type=task --priority=<match parent>` then `bd dep add <new-id> <current-id>` as appropriate. Never leave discovered work untracked.
### 2c. Verify (hard gates — ALL must pass)
```bash
<TEST_CMD> # full test suite, not just new tests
<BUILD_CMD> # full build
<LINT_CMD> # if present
```
- ✅ All tests pass (including pre-existing ones — no regressions)
- ✅ Build succeeds cleanly
- ✅ Every acceptance criterion on the issue is demonstrably met
- ✅ No new warnings/lint errors introduced
On failure: fix and re-verify. After **3 failed fix attempts** on the same root cause, stop thrashing — record findings with `bd update <id> --notes="<what was tried, error output, hypothesis>"`, run `bd human <id>` if it needs a decision, revert the working tree to the last green state, and move to the next issue.
### 2d. Close & Checkpoint
```bash
bd close <id> --reason="<one-line summary of what was done>" --suggest-next
bd remember "<any non-obvious insight learned: gotcha, pattern, decision>"
git add <changed files>
git commit -m "<type>(<scope>): <summary> (closes <id>)"
```
- Commit per issue, not in batches — each commit should be independently green.
- `--suggest-next` output feeds Phase 2e.
### 2e. Re-plan
- Newly unblocked issues from `--suggest-next` are inserted into the queue in priority order.
- Every **5 closed issues**, re-run `bd ready` and `bd blocked` to catch drift (issues added by hooks, remote changes), and re-run the full test suite even if the last issue didn't touch shared code.
### Parallelism
When the queue contains **independent** issues (no shared files, no dependency relation), dispatch up to 3 in parallel via the Task tool (subagents), each following 2a–2d in isolation. Serialize anything touching shared modules, migrations, or config. Merge subagent commits sequentially and re-run TEST_CMD after each merge.
## Phase 3 — Exit Conditions & Escalation
The loop terminates when **one** of these holds:
1. **Success**: `bd ready` is empty AND `bd blocked` is empty (within the `$ARGUMENTS` scope) AND the final full TEST_CMD + BUILD_CMD run is green.
2. **Human-gated**: remaining issues are all flagged via `bd human` — report them with the specific decision needed for each.
3. **Externally blocked**: remaining blockers depend on things outside this repo (third-party, credentials, other teams) — document each with `bd update <id> --notes` and report.
Never terminate by silently skipping issues. Every open issue at exit must be either human-flagged or externally blocked with notes explaining why.
## Phase 4 — Session Close Protocol (MANDATORY)
```bash
[ ] git status # confirm nothing unstaged/untracked left behind
[ ] git add <files> # stage any stragglers
[ ] bd dolt pull # pull beads updates from main
[ ] git commit -m "..." # final commit if anything staged
[ ] bd stats # final counts
```
Then produce the **Drain Report**:
- **Burn-down**: baseline vs final counts (open / in_progress / blocked / closed this session)
- **Closed**: each issue ID + one-line summary + commit SHA
- **Escalated**: each `bd human` flag with the exact decision needed
- **Externally blocked**: each with the external dependency named
- **Discovered work**: new issues created during the drain
- **Insights recorded**: `bd remember` entries added
- **Final verification**: test suite result, build result, lint result
## Hard Rules
- Use beads for ALL tracking. Never TodoWrite, TaskCreate, or markdown task files.
- Never `bd edit` (blocks on $EDITOR).
- Never close an issue whose acceptance criteria aren't verifiably met — a passing build is necessary, not sufficient.
- Never commit with a red test suite.
- Priority values are numeric (0–4 / P0–P4), never "high"/"medium"/"low".
- Persistence you don't need beats lost context: over-record notes, insights, and reasons.
+140
View File
@@ -0,0 +1,140 @@
---
description: Extract insight-type learnings from claude-mem and record them into beads memories
---
# Sync claude-mem insights → beads memories
<!--toc:start-->
- [Sync claude-mem insights → beads memories](#sync-claude-mem-insights-beads-memories)
- [Scope (read carefully)](#scope-read-carefully)
- [Phase 0 — Preflight](#phase-0-preflight)
- [Phase 1 — Enumerate ALL learnings (the loop)](#phase-1-enumerate-all-learnings-the-loop)
- [Phase 2 — Triage & record (per-item loop)](#phase-2-triage-record-per-item-loop)
- [Phase 3 — Finalize](#phase-3-finalize)
- [Guardrails](#guardrails)
<!--toc:end-->
You are migrating **durable insights** from **claude-mem** (the session memory store)
into **beads** (`bd remember`) for this repository. Work autonomously through a loop
until every relevant observation has been triaged. Do not invent learnings — only
persist what claude-mem actually returns.
## Scope (read carefully)
- **Record ONLY insights / decisions / gotchas** → `bd remember`.
- **Ignore actionable learnings** (bugs to fix, tech debt, follow-ups, TODOs).
Do **not** create them as beads issues. A separate command will later pull those
from claude-mem and feed them into **Linear** for PM; beads then syncs from Linear.
- This command therefore **never calls `bd create`** and never writes beads issues.
The only write to beads is `bd remember`. Leave actionable items untouched in
claude-mem so the future Linear command can pick them up.
---
## Phase 0 — Preflight
1. Confirm beads is live in this repo: `bd ready` (and `bd doctor` if it errors).
2. Snapshot existing memories for dedup: `bd memories ""` (plus a few broad keyword
`bd memories <term>` lookups) and keep the gist in mind.
3. Set up a **local progress file** for resumability (so compaction/re-runs skip done
work) — NOT a beads issue:
```
./.claude-mem-sync/processed.json # { "processed": [<cmem ids>], "skipped_actionable": <count> }
```
Create it if missing; load it if present. Add `.claude-mem-sync/` to `.gitignore`
if it isn't already.
---
## Phase 1 — Enumerate ALL learnings (the loop)
claude-mem search is query-based, so a single call won't surface everything. Build a
**complete, de-duplicated set of observation IDs** by sweeping across angles, then
walking chronologically. Track seen IDs in a set; never process the same ID twice.
Use `/claude-mem:mem-search` for every query below, following claude-mem's 3-layer
pattern — pull the compact **index** first, only fetch full details for IDs you keep.
**1a. Type sweep** — one broad query per observation type, high limit:
```
/claude-mem:mem-search (type=insight)
/claude-mem:mem-search (type=decision)
/claude-mem:mem-search (type=bugfix) # scan for the *lesson learned*, not the fix-task
/claude-mem:mem-search (type=architecture)
/claude-mem:mem-search (type=feature) # plus any other types this project uses
```
> You still scan every type — a bugfix or feature observation often contains a durable
> *insight* ("X breaks when Y") even though its actionable part is ignored here.
**1b. Topic sweep** — queries for the repo's recurring themes (infer from the codebase:
auth, deploy, config, performance, gotchas, "why we", "decided", "learned", "don't").
Each adds new IDs to the set.
**1c. Chronological walk** — to catch what the sweeps missed, seed `timeline` from your
earliest and latest known observations and step through history, collecting any IDs not
already in the set. Stop when the timeline yields no new IDs in either direction.
Result: a deduplicated list of candidate observation IDs. Print the count.
---
## Phase 2 — Triage & record (per-item loop)
Process candidate IDs in batches. For each batch:
1. **Fetch details** once: `get_observations(ids=[...])` (batch IDs — never one-at-a-time).
2. For **each** observation, classify:
- **Actionable** (a thing to *do*: unfixed bug, tech debt, follow-up, TODO) →
**IGNORE**. Increment `skipped_actionable` in the progress file and move on.
It stays in claude-mem for the Linear command.
- **Insight / decision / gotcha** (durable knowledge about *how things are*) →
record it:
- **Dedup check** first: `bd memories <key terms>`. If beads already captures it, skip.
- Then:
```
bd remember "<one-line durable insight, self-contained, no session context> [cmem #<id>]"
```
Keep it atomic and reusable. Include the source `cmem #<id>` so it traces back
to http://localhost:37777/api/observation/<id>.
- **Noise** (one-off output, ephemeral state, trivially obvious, duplicate) → skip silently.
3. Append processed IDs to `./.claude-mem-sync/processed.json`.
Repeat until the candidate set is exhausted.
---
## Phase 3 — Finalize
1. Sanity pass: `bd memories ""` — confirm new insights landed and read cleanly.
2. Sync beads memories to remote: `bd dolt push`.
3. **Session close protocol** — if any code/files changed (e.g. `.gitignore`):
```
git status
git add <files>
git commit -m "chore: record claude-mem insights into beads"
git push
```
---
## Guardrails
- **Insights only.** Never `bd create`, never write a beads issue. Actionable items are
ignored and left in claude-mem for the Linear sync command.
- **Idempotent.** Re-running must not duplicate — always dedup against `bd memories` and
the local progress file before writing.
- **Atomic memories.** One insight per `bd remember`, self-contained, no "in this session"
phrasing.
- **Cite the source.** Every entry carries its `cmem #<id>` for traceability.
- **No `bd edit`** — it opens an editor and blocks.
- **Don't fabricate** — if a sweep returns nothing, say so and move on.
- For large sets, run the type/topic sweeps and `bd memories` dedup lookups with
**parallel subagents** for speed.
When done, report: total observations scanned, how many insights were recorded, how many
were skipped as **actionable** (this is the backlog the Linear command will handle), and
how many were skipped as noise/duplicates.
+179
View File
@@ -0,0 +1,179 @@
---
description: Extract actionable learnings from claude-mem into beads tasks and push them up to Linear for PM
---
# Sync claude-mem actionable learnings → beads → Linear
<!--toc:start-->
- [Sync claude-mem actionable learnings → beads → Linear](#sync-claude-mem-actionable-learnings-beads-linear)
- [Scope (read carefully)](#scope-read-carefully)
- [Sync model (no linear-cli needed)](#sync-model-no-linear-cli-needed)
- [Phase 0 — Preflight](#phase-0-preflight)
- [Phase 1 — Enumerate ALL learnings (the loop)](#phase-1-enumerate-all-learnings-the-loop)
- [Phase 2 — Triage & create (per-item loop)](#phase-2-triage-create-per-item-loop)
- [Phase 3 — Push to Linear](#phase-3-push-to-linear)
- [Phase 4 — Finalize](#phase-4-finalize)
- [Guardrails](#guardrails)
<!--toc:end-->
You are turning **actionable learnings** from **claude-mem** (the session memory store)
into **beads** (`bd`) tasks, then pushing the new ones up to **Linear** for PM. Work
autonomously through a loop until every relevant observation has been triaged. Do not
invent learnings — only persist what claude-mem actually returns.
## Scope (read carefully)
- **Record ONLY actionable learnings** — things to *do*: unfixed bugs, tech debt,
follow-ups, TODOs, missing tests, planned refactors. Each becomes a `bd create` task.
- **Ignore insights / decisions / gotchas** (durable knowledge about *how things are*).
A separate command records those as `bd remember` memories — do **not** touch them here.
Leave them in claude-mem untouched.
- A single observation may legitimately contain both an insight *and* an action item; the
two commands are independent, so it's fine if this one creates a task while the other
records a memory from the same `cmem #<id>`.
## Sync model (no linear-cli needed)
Beads-first, push up, bidirectional thereafter:
1. Create tasks in beads (`bd create`).
2. Push the new ones to Linear with `bd linear push <ids>` — this creates the Linear
issues and writes `external_ref` back onto each bead (`--update-refs` is default true).
3. From then on, `bd linear sync` is bidirectional: PM edits in Linear pull back into beads.
`linear-cli` is **not** used. All Linear interaction goes through native `bd linear`.
---
## Phase 0 — Preflight
1. Confirm beads is live: `bd ready` (and `bd doctor` if it errors).
2. Confirm Linear is wired up: `bd linear status`. If it errors / unconfigured, stop and
tell the user to set `linear.api_key` + team (`bd linear teams` to find the team UUID).
Note whether **multiple teams** are configured (`linear.team_ids`) — if so, push needs
an explicit `--team <ID>`; ask the user which team these tasks belong to.
3. Dedup snapshot: `bd list --status=open` and a few `bd search <term>` lookups so you can
recognize existing tasks (including ones already pulled from Linear).
4. Local progress file for resumability — NOT a beads issue:
```
./.claude-mem-sync/processed-actions.json # { "processed": [<cmem ids>], "created": [<bead ids>], "skipped_insights": <n> }
```
Create if missing, load if present. Ensure `.claude-mem-sync/` is in `.gitignore`.
---
## Phase 1 — Enumerate ALL learnings (the loop)
claude-mem search is query-based, so one call won't surface everything. Build a
**complete, de-duplicated set of observation IDs** by sweeping across angles, then walking
chronologically. Track seen IDs in a set; never process the same ID twice.
Use `/claude-mem:mem-search` for every query below, following claude-mem's 3-layer pattern
— pull the compact **index** first, only fetch full details for IDs you keep.
**1a. Type sweep** — one broad query per observation type, high limit:
```
/claude-mem:mem-search (type=bugfix)
/claude-mem:mem-search (type=feature)
/claude-mem:mem-search (type=insight) # scan for the *action item* buried inside, not the insight
/claude-mem:mem-search (type=decision)
/claude-mem:mem-search (type=architecture) # plus any other types this project uses
```
> Scan every type — even an `insight` observation often ends with "…so we should do X",
> which is the action item you extract here while ignoring the insight itself.
**1b. Topic sweep** — queries for action-shaped language the codebase tends to record:
"TODO", "should", "need to", "fix", "broken", "flaky", "refactor", "tech debt", "later",
"workaround", "temporary", "follow up". Each adds new IDs to the set.
**1c. Chronological walk** — seed `timeline` from your earliest and latest known
observations and step through history, collecting any IDs not already in the set. Stop when
the timeline yields no new IDs in either direction.
Result: a deduplicated list of candidate observation IDs. Print the count.
---
## Phase 2 — Triage & create (per-item loop)
Process candidate IDs in batches. For each batch:
1. **Fetch details** once: `get_observations(ids=[...])` (batch IDs — never one-at-a-time).
2. For **each** observation, classify:
- **Insight / decision / gotcha** (knowledge, nothing to *do*) → **IGNORE**. Increment
`skipped_insights` and move on. It's the other command's job.
- **Noise** (one-off output, ephemeral state, trivially obvious, duplicate) → skip silently.
- **Actionable** → create a task:
- **Dedup first:** `bd search "<key terms>"` and `bd search "cmem #<id>"`. If a matching
task already exists (locally or pulled from Linear), skip — this keeps re-runs idempotent.
- Then:
```
bd create --title="<imperative summary>" \
--description="<what + why, ending with: Source: cmem #<id>>" \
--type=bug|task|feature \
--priority=<0-4>
```
- Priority is `0-4` / `P0-P4` (0=critical … 4=backlog) — never high/medium/low.
bd maps this to Linear priority on push.
- Optionally add `--acceptance="<done criteria>"` when the observation implies one.
- If two items depend on each other: `bd dep add <issue> <depends-on>`.
- Record the new bead id and the `cmem #<id>` in the progress file.
Repeat until the candidate set is exhausted.
---
## Phase 3 — Push to Linear
1. Review what's queued: `bd linear status` (shows issues pending push, i.e. no external_ref).
2. Push **only the tasks you just created**, by id, so you don't drag along unrelated pending work:
```
bd linear push <new-bead-id-1> <new-bead-id-2> ...
```
- Add `--team <ID>` if multiple teams are configured (from Phase 0).
- Run `--dry-run` first if the user wants a preview.
- This creates the Linear issues and sets `external_ref` on each bead (default `--update-refs`).
- Alternative for bulk: `bd linear sync --push --create-only` (+ `--team`), but prefer the
explicit id list for precision.
3. Confirm linkage: re-run `bd linear status` and spot-check a couple of beads now carry an
`external_ref` to Linear.
> Steady state: after this seeding push, a scheduled or manual `bd linear sync` keeps both
> sides aligned — PM's changes in Linear pull back down to beads. You don't push again for
> these; you only push *newly created* tasks on future runs.
---
## Phase 4 — Finalize
1. Sync beads to its Dolt remote: `bd dolt push`.
2. **Session close protocol** — if any code/files changed (e.g. `.gitignore`):
```
git status
git add <files>
git commit -m "chore: extract claude-mem action items into beads + Linear"
git push
```
---
## Guardrails
- **Actionable only.** Never `bd remember` here. Insights are ignored and left in claude-mem
for the insights command.
- **No linear-cli.** All Linear interaction is native `bd linear`.
- **Idempotent.** Re-running must not duplicate — dedup against `bd search` (including
`bd search "cmem #<id>"`) and the progress file before creating, and push only new ids.
- **Cite the source.** Every task description ends with `Source: cmem #<id>`
(traceable at http://localhost:37777/api/observation/<id>).
- **No `bd edit`** — it opens an editor and blocks. Use inline `--` flags on create/update.
- **Multiple teams → explicit `--team`** on push; ask the user if ambiguous.
- **Don't fabricate** — if a sweep returns nothing, say so and move on.
- For large sets, run the type/topic sweeps and `bd search` dedup lookups with **parallel
subagents** for speed (but run `bd create` / `bd linear push` serially to keep ids ordered).
When done, report: total observations scanned, how many tasks were created (with bead ids),
how many pushed to Linear (with external_refs), how many skipped as **insights** (the other
command's backlog), and how many skipped as noise/duplicates.
+102
View File
@@ -0,0 +1,102 @@
---
description: Plan a task by decomposing it into beads issues with dependencies, acceptance criteria, and priorities
argument-hint: <task description or goal>
allowed-tools: Bash(bd:*), Bash(git log:*), Bash(git status:*), Bash(rg:*), Task
---
# /create-task — Plan work into beads
<!--toc:start-->
- [/create-task — Plan work into beads](#create-task-plan-work-into-beads)
- [Current workspace state](#current-workspace-state)
- [Task to plan](#task-to-plan)
- [Instructions](#instructions)
- [1. Understand before creating](#1-understand-before-creating)
- [2. Decompose](#2-decompose)
- [3. Create issues](#3-create-issues)
- [4. Wire dependencies](#4-wire-dependencies)
- [5. Report the plan](#5-report-the-plan)
<!--toc:end-->
## Current workspace state
- Ready work: !`bd ready 2>/dev/null | head -30`
- In progress: !`bd list --status=in_progress 2>/dev/null | head -15`
- Recently closed (dedup check): !`bd list --status=closed 2>/dev/null | head -10`
- Stats: !`bd stats 2>/dev/null`
- Relevant memories: !`bd memories "$ARGUMENTS" 2>/dev/null | head -40`
## Task to plan
$ARGUMENTS
## Instructions
You are planning this task using **beads as the sole tracker**. Do NOT use TodoWrite, TaskCreate, or markdown checklists. Do NOT write any code in this command — output is a set of beads issues forming an executable plan.
### 1. Understand before creating
- Search for prior art first: `bd search "<keywords>"` — the task (or part of it) may already exist. Per repo convention, `contracts-*`/`backend-*`/`core-*` beads mirror Linear CORE-* and share ID suffixes across sibling repos; if something looks missing, check sibling repo beads and `git log` before assuming it needs a new issue.
- Check the injected memories above for constraints, gotchas, and prior decisions that shape the plan (e.g., root-file serialization, generated-file rules, runtime conventions).
- If the request is ambiguous on scope, ask ONE clarifying question, then proceed.
### 2. Decompose
Break the task into issues sized for a single focused session (roughly ≤ half a day each). For each issue decide:
- **type**: `feature` (new capability), `task` (chore/refactor/infra), `bug` (defect)
- **priority**: numeric only — `0` critical / `1` high / `2` medium / `3` low / `4` backlog. Never "high"/"medium"/"low".
- **description**: WHY the issue exists + WHAT done looks like. Never a bare title restatement.
- **acceptance criteria**: concrete, verifiable (tests pass, command output, deployed address updated, etc.)
- **design** (when a decision was made): record it so future sessions don't re-litigate.
If the plan touches shared root files (`turbo.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, root Biome/tsconfig), note it in the issue and **serialize those issues with dependencies** — they cannot run in parallel.
### 3. Create issues
Use the full quality flags:
```bash
bd create \
--title="<imperative summary>" \
--description="<why + what>" \
--type=<task|bug|feature> \
--priority=<0-4> \
--acceptance="<verifiable criteria>" \
--design="<decisions, if any>" \
--notes="<gotchas from memories, file paths, related beads/Linear IDs>" \
--validate
```
- **3+ issues → spawn parallel subagents**, one `bd create` batch each, then collect the returned IDs.
- Quote every string flag (descriptions contain `&`, `$`, backticks).
- NEVER use `bd edit` (opens $EDITOR and blocks). Amend with `bd update <id> --description/--notes/--design`.
### 4. Wire dependencies
```bash
bd dep add <dependent> <blocker> # dependent depends on blocker
```
- Model the real execution order: tests depend on implementation, redeploys depend on ABI changes, root-file edits form a chain.
- Verify with `bd blocked` and `bd show <id>` that the graph matches intent — no cycles, no orphans (`bd orphans`).
### 5. Report the plan
Output a concise summary:
1. **Created issues** — table of `id | title | type | priority`
2. **Dependency graph** — short text sketch of the ordering (e.g. `x8y → oto → 53x`)
3. **Entry point** — which issue(s) show in `bd ready` right now, i.e., where work starts
4. **Deferred/skipped** — anything that already existed (with the existing ID) or was intentionally left out
Then persist and finish:
```bash
bd ready # confirm entry points are unblocked
git status # beads auto-commits to Dolt; push if session ends here
```
If planning surfaced a durable insight (a constraint, a decision, a gotcha), store it: `bd remember "<insight>"` — never a MEMORY.md file.
Do NOT start implementing any issue. Planning ends when the graph is created, validated, and reported. Implementation begins in a later session via `bd ready` → `bd update <id> --claim`.
+104
View File
@@ -0,0 +1,104 @@
---
description: Run accessibility and visual design review
---
# Rams Design Review
You are Rams, an expert design engineer reviewing code for accessibility and visual design issues.
## Mode
If `$ARGUMENTS` is provided, analyze that specific file.
If `$ARGUMENTS` is empty, ask the user which file(s) to review, or offer to scan the project for component files.
---
## 1. Accessibility Review (WCAG 2.1)
### Critical (Must Fix)
| Check | WCAG | What to look for |
|-------|------|------------------|
| Images without alt | 1.1.1 | `<img>` without `alt` attribute |
| Icon-only buttons | 4.1.2 | `<button>` with only SVG/icon, no `aria-label` |
| Form inputs without labels | 1.3.1 | `<input>`, `<select>`, `<textarea>` without associated `<label>` or `aria-label` |
| Non-semantic click handlers | 2.1.1 | `<div onClick>` or `<span onClick>` without `role`, `tabIndex`, `onKeyDown` |
| Missing link destination | 2.1.1 | `<a>` without `href` using only `onClick` |
### Serious (Should Fix)
| Check | WCAG | What to look for |
|-------|------|------------------|
| Focus outline removed | 2.4.7 | `outline-none` or `outline: none` without visible focus replacement |
| Missing keyboard handlers | 2.1.1 | Interactive elements with `onClick` but no `onKeyDown`/`onKeyUp` |
| Color-only information | 1.4.1 | Status/error indicated only by color (no icon/text) |
| Touch target too small | 2.5.5 | Clickable elements smaller than 44x44px |
### Moderate (Consider Fixing)
| Check | WCAG | What to look for |
|-------|------|------------------|
| Heading hierarchy | 1.3.1 | Skipped heading levels (h1 → h3) |
| Positive tabIndex | 2.4.3 | `tabIndex` > 0 (disrupts natural tab order) |
| Role without required attributes | 4.1.2 | `role="button"` without `tabIndex="0"` |
---
## 2. Visual Design Review
### Layout & Spacing
- Inconsistent spacing values
- Overflow issues, alignment problems
- Z-index conflicts
### Typography
- Mixed font families, weights, or sizes
- Line height issues
- Missing font fallbacks
### Color & Contrast
- Contrast ratio below 4.5:1
- Missing hover/focus states
- Dark mode inconsistencies
### Components
- Missing button states (disabled, loading, hover, active, focus)
- Missing form field states (error, success, disabled)
- Inconsistent borders, shadows, or icon sizing
---
## Output Format
```
═══════════════════════════════════════════════════
RAMS DESIGN REVIEW: [filename]
═══════════════════════════════════════════════════
CRITICAL (X issues)
───────────────────
[A11Y] Line 24: Button missing accessible name
<button><CloseIcon /></button>
Fix: Add aria-label="Close"
WCAG: 4.1.2
SERIOUS (X issues)
──────────────────
...
═══════════════════════════════════════════════════
SUMMARY: X critical, X serious, X moderate
Score: XX/100
═══════════════════════════════════════════════════
```
---
## Guidelines
1. Read the file(s) first before making assessments
2. Be specific with line numbers and code snippets
3. Provide fixes, not just problems
4. Prioritize critical accessibility issues first
If asked, offer to fix the issues directly.
@@ -0,0 +1,180 @@
---
description: Review UI code for Vercel Web Interface Guidelines compliance
argument-hint: <file-or-pattern>
---
# Web Interface Guidelines
Review these files for compliance: $ARGUMENTS
Read files, check against rules below. Output concise but comprehensive—sacrifice grammar for brevity. High signal-to-noise.
## Rules
### Accessibility
- Icon-only buttons need `aria-label`
- Form controls need `<label>` or `aria-label`
- Interactive elements need keyboard handlers (`onKeyDown`/`onKeyUp`)
- `<button>` for actions, `<a>`/`<Link>` for navigation (not `<div onClick>`)
- Images need `alt` (or `alt=""` if decorative)
- Decorative icons need `aria-hidden="true"`
- Async updates (toasts, validation) need `aria-live="polite"`
- Use semantic HTML (`<button>`, `<a>`, `<label>`, `<table>`) before ARIA
- Headings hierarchical `<h1>`–`<h6>`; include skip link for main content
- `scroll-margin-top` on heading anchors
### Focus States
- Interactive elements need visible focus: `focus-visible:ring-*` or equivalent
- Never `outline-none` / `outline: none` without focus replacement
- Use `:focus-visible` over `:focus` (avoid focus ring on click)
- Group focus with `:focus-within` for compound controls
### Forms
- Inputs need `autocomplete` and meaningful `name`
- Use correct `type` (`email`, `tel`, `url`, `number`) and `inputmode`
- Never block paste (`onPaste` + `preventDefault`)
- Labels clickable (`htmlFor` or wrapping control)
- Disable spellcheck on emails, codes, usernames (`spellCheck={false}`)
- Checkboxes/radios: label + control share single hit target (no dead zones)
- Submit button stays enabled until request starts; spinner during request
- Errors inline next to fields; focus first error on submit
- Placeholders end with `…` and show example pattern
- `autocomplete="off"` on non-auth fields to avoid password manager triggers
- Warn before navigation with unsaved changes (`beforeunload` or router guard)
### Animation
- Honor `prefers-reduced-motion` (provide reduced variant or disable)
- Animate `transform`/`opacity` only (compositor-friendly)
- Never `transition: all`—list properties explicitly
- Set correct `transform-origin`
- SVG: transforms on `<g>` wrapper with `transform-box: fill-box; transform-origin: center`
- Animations interruptible—respond to user input mid-animation
### Typography
- `…` not `...`
- Curly quotes `"` `"` not straight `"`
- Non-breaking spaces: `10&nbsp;MB`, `⌘&nbsp;K`, brand names
- Loading states end with `…`: `"Loading…"`, `"Saving…"`
- `font-variant-numeric: tabular-nums` for number columns/comparisons
- Use `text-wrap: balance` or `text-pretty` on headings (prevents widows)
### Content Handling
- Text containers handle long content: `truncate`, `line-clamp-*`, or `break-words`
- Flex children need `min-w-0` to allow text truncation
- Handle empty states—don't render broken UI for empty strings/arrays
- User-generated content: anticipate short, average, and very long inputs
### Images
- `<img>` needs explicit `width` and `height` (prevents CLS)
- Below-fold images: `loading="lazy"`
- Above-fold critical images: `priority` or `fetchpriority="high"`
### Performance
- Large lists (>50 items): virtualize (`virtua`, `content-visibility: auto`)
- No layout reads in render (`getBoundingClientRect`, `offsetHeight`, `offsetWidth`, `scrollTop`)
- Batch DOM reads/writes; avoid interleaving
- Prefer uncontrolled inputs; controlled inputs must be cheap per keystroke
- Add `<link rel="preconnect">` for CDN/asset domains
- Critical fonts: `<link rel="preload" as="font">` with `font-display: swap`
### Navigation & State
- URL reflects state—filters, tabs, pagination, expanded panels in query params
- Links use `<a>`/`<Link>` (Cmd/Ctrl+click, middle-click support)
- Deep-link all stateful UI (if uses `useState`, consider URL sync via nuqs or similar)
- Destructive actions need confirmation modal or undo window—never immediate
### Touch & Interaction
- `touch-action: manipulation` (prevents double-tap zoom delay)
- `-webkit-tap-highlight-color` set intentionally
- `overscroll-behavior: contain` in modals/drawers/sheets
- During drag: disable text selection, `inert` on dragged elements
- `autoFocus` sparingly—desktop only, single primary input; avoid on mobile
### Safe Areas & Layout
- Full-bleed layouts need `env(safe-area-inset-*)` for notches
- Avoid unwanted scrollbars: `overflow-x-hidden` on containers, fix content overflow
- Flex/grid over JS measurement for layout
### Dark Mode & Theming
- `color-scheme: dark` on `<html>` for dark themes (fixes scrollbar, inputs)
- `<meta name="theme-color">` matches page background
- Native `<select>`: explicit `background-color` and `color` (Windows dark mode)
### Locale & i18n
- Dates/times: use `Intl.DateTimeFormat` not hardcoded formats
- Numbers/currency: use `Intl.NumberFormat` not hardcoded formats
- Detect language via `Accept-Language` / `navigator.languages`, not IP
- Brand names, code tokens, identifiers: wrap with `translate="no"` to prevent garbled auto-translation
### Hydration Safety
- Inputs with `value` need `onChange` (or use `defaultValue` for uncontrolled)
- Date/time rendering: guard against hydration mismatch (server vs client)
- `suppressHydrationWarning` only where truly needed
### Hover & Interactive States
- Buttons/links need `hover:` state (visual feedback)
- Interactive states increase contrast: hover/active/focus more prominent than rest
### Content & Copy
- Active voice: "Install the CLI" not "The CLI will be installed"
- Title Case for headings/buttons (Chicago style)
- Numerals for counts: "8 deployments" not "eight"
- Specific button labels: "Save API Key" not "Continue"
- Error messages include fix/next step, not just problem
- Second person; avoid first person
- `&` over "and" where space-constrained
### Anti-patterns (flag these)
- `user-scalable=no` or `maximum-scale=1` disabling zoom
- `onPaste` with `preventDefault`
- `transition: all`
- `outline-none` without focus-visible replacement
- Inline `onClick` navigation without `<a>`
- `<div>` or `<span>` with click handlers (should be `<button>`)
- Images without dimensions
- Large arrays `.map()` without virtualization
- Form inputs without labels
- Icon buttons without `aria-label`
- Hardcoded date/number formats (use `Intl.*`)
- `autoFocus` without clear justification
## Output Format
Group by file. Use `file:line` format (VS Code clickable). Terse findings.
```text
## src/Button.tsx
src/Button.tsx:42 - icon button missing aria-label
src/Button.tsx:18 - input lacks label
src/Button.tsx:55 - animation missing prefers-reduced-motion
src/Button.tsx:67 - transition: all → list properties
## src/Modal.tsx
src/Modal.tsx:12 - missing overscroll-behavior: contain
src/Modal.tsx:34 - "..." → "…"
## src/Card.tsx
✓ pass
```
State issue + location. Skip explanation unless fix non-obvious. No preamble.
View File
Whitespace-only changes.
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
# Reads a Claude Code SessionEnd hook JSON on stdin and exports that session's
# claude-mem learnings to a single Obsidian note. Stdlib only. Fails soft:
# any error exits 0 without blocking the session.
#
# Data interface (discovered live, not guessed):
# - SQLite DB: ~/.claude-mem/claude-mem.db (read-only)
# - sdk_sessions.content_session_id == Claude Code session_id
# - sdk_sessions.memory_session_id -> observations / session_summaries
# - observation types seen: discovery, change, feature, decision, bugfix, refactor
# - no private/excluded column exists: claude-mem drops <private> content at
# capture time so it never reaches the DB. We still scan defensively.
# - viewer deep link that resolves: http://localhost:<port>/?observation=<id>
import sys, os, re, json, sqlite3, datetime
from pathlib import Path
HOME = Path.home()
DB_PATH = Path(os.environ.get("CLAUDE_MEM_DB", HOME / ".claude-mem" / "claude-mem.db"))
VAULT_DIR = Path(os.environ.get("CMEM_OBSIDIAN_DIR", HOME / "Documents" / "Vault" / "90 - Archive" / "Learnings"))
WORKER_PORT = os.environ.get("CLAUDE_MEM_WORKER_PORT", "37700")
LOG_PATH = HOME / ".claude" / "hooks" / "cmem-to-obsidian.log"
# observation type -> note section
SECTIONS = [
("Decisions", {"decision"}),
("Fixes & gotchas", {"bugfix"}),
("Insights", {"discovery", "feature", "change", "refactor"}),
]
def log(msg):
try:
with open(LOG_PATH, "a") as f:
f.write(f"{datetime.datetime.now().isoformat()} {msg}\n")
except Exception:
pass
def slug(s):
return re.sub(r"[^a-z0-9]+", "-", (s or "").lower()).strip("-") or "unknown"
def is_private(obs):
blob = " ".join(str(obs.get(k) or "") for k in ("title", "subtitle", "narrative", "text", "facts"))
return "<private>" in blob.lower()
def connect_ro(path):
# read-only so we never lock or mutate the live DB / WAL
return sqlite3.connect(f"file:{path}?mode=ro", uri=True, timeout=5)
def fetch(session_id):
con = connect_ro(DB_PATH)
con.row_factory = sqlite3.Row
cur = con.cursor()
row = cur.execute(
"SELECT memory_session_id, project, started_at FROM sdk_sessions "
"WHERE content_session_id = ? LIMIT 1", (session_id,)
).fetchone()
if not row or not row["memory_session_id"]:
con.close()
return None
mem_id, project, started_at = row["memory_session_id"], row["project"], row["started_at"]
obs = [dict(r) for r in cur.execute(
"SELECT id, type, title, subtitle, narrative FROM observations "
"WHERE memory_session_id = ? ORDER BY created_at_epoch ASC", (mem_id,)
).fetchall()]
summ = cur.execute(
"SELECT learned, completed, investigated, request FROM session_summaries "
"WHERE memory_session_id = ? ORDER BY created_at_epoch DESC LIMIT 1", (mem_id,)
).fetchone()
con.close()
return {
"mem_id": mem_id,
"project": project or "unknown",
"started_at": started_at,
"observations": [o for o in obs if not is_private(o)],
"summary": dict(summ) if summ else {},
}
def obs_link(oid):
return f"http://localhost:{WORKER_PORT}/?observation={oid}"
def one_line(obs):
title = (obs.get("title") or "").strip()
sub = (obs.get("subtitle") or "").strip()
text = f"{title} — {sub}" if title and sub else (title or sub or "(untitled observation)")
return f"- {text} [cmem #{obs['id']}]({obs_link(obs['id'])})"
def render_note(data, session_id):
project = data["project"]
pslug = slug(project)
# date from the session's own start time -> stable across re-runs (idempotent)
date = (data["started_at"] or datetime.datetime.now().isoformat())[:10]
short = session_id[:8]
summary = data["summary"]
abstract = (summary.get("learned") or summary.get("completed")
or summary.get("investigated") or summary.get("request") or "").strip()
abstract = re.sub(r"\s+", " ", abstract) or "No session summary recorded."
fm = [
"---",
f"date: {date}",
f"project: {project}",
f"session: {short}",
"source: claude-mem",
"tags:",
" - claude-mem",
f" - project/{pslug}",
"---",
"",
f"# {project} — Session Learnings ({date})",
"",
"> [!abstract] Session summary",
f"> {abstract}",
"",
f"Project: [[{project}]]",
"",
]
body = []
by_id = {o["id"]: o for o in data["observations"]}
placed = set()
for heading, types in SECTIONS:
items = [o for o in data["observations"] if o["type"] in types]
if not items:
continue
body.append(f"## {heading}")
for o in items:
placed.add(o["id"])
if heading == "Decisions":
t = (o.get("title") or "").strip()
s = (o.get("subtitle") or "").strip()
body.append(f"- > [!note] {t}" + (f" — {s}" if s else "")
+ f" [cmem #{o['id']}]({obs_link(o['id'])})")
else:
body.append(one_line(o))
body.append("")
# any type we didn't map -> Insights catch-all
leftover = [o for o in data["observations"] if o["id"] not in placed]
if leftover:
body.append("## Insights")
body.extend(one_line(o) for o in leftover)
body.append("")
return "\n".join(fm + body).rstrip() + "\n", project, pslug, date, short
def ensure_project_moc(project):
# so [[Project]] wikilink resolves
moc = VAULT_DIR / f"{project}.md"
try:
if not moc.exists():
moc.write_text(
f"---\ntags:\n - moc\n - project/{slug(project)}\n---\n\n# {project}\n\n"
"Map of content for claude-mem session learnings.\n"
)
except Exception as e:
log(f"moc write failed: {e}")
def write_note(data, session_id):
text, project, pslug, date, short = render_note(data, session_id)
folder = VAULT_DIR / project
folder.mkdir(parents=True, exist_ok=True)
ensure_project_moc(project)
# deterministic filename keyed to session -> idempotent upsert
path = folder / f"{date} {project} {short}.md"
path.write_text(text)
log(f"wrote {path} ({len(data['observations'])} observations)")
return path
def main():
try:
payload = json.load(sys.stdin)
except Exception:
return
session_id = payload.get("session_id")
if not session_id:
return
try:
if not DB_PATH.exists():
return
data = fetch(session_id)
if not data or not data["observations"]:
return
write_note(data, session_id)
except Exception as e:
log(f"export failed for session {session_id}: {e}")
return
if __name__ == "__main__":
main()
@@ -0,0 +1,101 @@
#!/bin/sh
# installed by herdr
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=claude
# HERDR_INTEGRATION_VERSION=7
set -eu
action="${1:-}"
hook_input_file="$(mktemp "${TMPDIR:-/tmp}/herdr-claude-hook.XXXXXX")" || exit 0
trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM
cat >"$hook_input_file" 2>/dev/null || true
case "$action" in
session) ;;
*) exit 0 ;;
esac
[ "${HERDR_ENV:-}" = "1" ] || exit 0
[ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0
[ -n "${HERDR_PANE_ID:-}" ] || exit 0
command -v python3 >/dev/null 2>&1 || exit 0
HERDR_ACTION="$action" HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY'
import json
import os
import random
import socket
import time
source = "herdr:claude"
action = os.environ.get("HERDR_ACTION", "")
pane_id = os.environ.get("HERDR_PANE_ID")
socket_path = os.environ.get("HERDR_SOCKET_PATH")
hook_input_file = os.environ.get("HERDR_HOOK_INPUT_FILE")
if not pane_id or not socket_path:
raise SystemExit(0)
hook_input = {}
if hook_input_file:
try:
with open(hook_input_file, encoding="utf-8") as handle:
content = handle.read()
if content.strip():
hook_input = json.loads(content)
except Exception:
hook_input = {}
hook_event_name = str(hook_input.get("hook_event_name") or "")
is_subagent = bool(hook_input.get("agent_id"))
if is_subagent:
raise SystemExit(0)
if hook_event_name == "SubagentStop":
# SubagentStop is a completion event. Older Herdr integrations mapped it
# to durable working, but Claude recap/away-summary can emit it after the
# main turn has already stopped. Never let it revive an idle pane.
raise SystemExit(0)
request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
report_seq = time.time_ns()
session_id = hook_input.get("session_id")
agent_session_id = session_id if isinstance(session_id, str) and session_id else None
transcript_path = hook_input.get("transcript_path")
agent_session_path = transcript_path if isinstance(transcript_path, str) and transcript_path else None
session_start_source = hook_input.get("source") if hook_event_name == "SessionStart" else None
if not isinstance(session_start_source, str) or not session_start_source:
session_start_source = None
if agent_session_id:
params = {
"pane_id": pane_id,
"source": source,
"agent": "claude",
"seq": report_seq,
"agent_session_id": agent_session_id,
}
if agent_session_path:
params["agent_session_path"] = agent_session_path
if session_start_source:
params["session_start_source"] = session_start_source
request = {
"id": request_id,
"method": "pane.report_agent_session",
"params": params,
}
else:
raise SystemExit(0)
try:
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.settimeout(0.5)
client.connect(socket_path)
client.sendall((json.dumps(request) + "\n").encode())
try:
client.recv(4096)
except Exception:
pass
client.close()
except Exception:
pass
PY
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env node
/**
* GitNexus Claude Code Hook
*
* PreToolUse handler — intercepts Grep/Glob/Bash searches
* and augments with graph context from the GitNexus index.
*
* NOTE: SessionStart hooks are broken on Windows (Claude Code bug).
* Session context is injected via CLAUDE.md / skills instead.
*/
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
/**
* Read JSON input from stdin synchronously.
*/
function readInput() {
try {
const data = fs.readFileSync(0, 'utf-8');
return JSON.parse(data);
} catch {
return {};
}
}
/**
* Check if a directory (or ancestor) has a .gitnexus index.
*/
function findGitNexusIndex(startDir) {
let dir = startDir || process.cwd();
for (let i = 0; i < 5; i++) {
if (fs.existsSync(path.join(dir, '.gitnexus'))) {
return true;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return false;
}
/**
* Extract search pattern from tool input.
*/
function extractPattern(toolName, toolInput) {
if (toolName === 'Grep') {
return toolInput.pattern || null;
}
if (toolName === 'Glob') {
const raw = toolInput.pattern || '';
const match = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/);
return match ? match[1] : null;
}
if (toolName === 'Bash') {
const cmd = toolInput.command || '';
if (!/\brg\b|\bgrep\b/.test(cmd)) return null;
const tokens = cmd.split(/\s+/);
let foundCmd = false;
let skipNext = false;
const flagsWithValues = new Set(['-e', '-f', '-m', '-A', '-B', '-C', '-g', '--glob', '-t', '--type', '--include', '--exclude']);
for (const token of tokens) {
if (skipNext) { skipNext = false; continue; }
if (!foundCmd) {
if (/\brg$|\bgrep$/.test(token)) foundCmd = true;
continue;
}
if (token.startsWith('-')) {
if (flagsWithValues.has(token)) skipNext = true;
continue;
}
const cleaned = token.replace(/['"]/g, '');
return cleaned.length >= 3 ? cleaned : null;
}
return null;
}
return null;
}
function main() {
try {
const input = readInput();
const hookEvent = input.hook_event_name || '';
if (hookEvent !== 'PreToolUse') return;
const cwd = input.cwd || process.cwd();
if (!findGitNexusIndex(cwd)) return;
const toolName = input.tool_name || '';
const toolInput = input.tool_input || {};
if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return;
const pattern = extractPattern(toolName, toolInput);
if (!pattern || pattern.length < 3) return;
// Resolve CLI path — try multiple strategies:
// 1. Relative path (works when script is inside npm package)
// 2. require.resolve (works when gitnexus is globally installed)
// 3. Fall back to npx (works when neither is available)
let cliPath = '/home/prad/.bun/install/global/node_modules/gitnexus/dist/cli/index.js';
if (!fs.existsSync(cliPath)) {
try {
cliPath = require.resolve('gitnexus/dist/cli/index.js');
} catch {
cliPath = ''; // will use npx fallback
}
}
// augment CLI writes result to stderr (KuzuDB's native module captures
// stdout fd at OS level, making it unusable in subprocess contexts).
const { spawnSync } = require('child_process');
let result = '';
try {
let child;
if (cliPath) {
child = spawnSync(
process.execPath,
[cliPath, 'augment', pattern],
{ encoding: 'utf-8', timeout: 8000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
);
} else {
// npx fallback
const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
child = spawnSync(
cmd,
['-y', 'gitnexus', 'augment', pattern],
{ encoding: 'utf-8', timeout: 15000, cwd, stdio: ['pipe', 'pipe', 'pipe'] }
);
}
result = child.stderr || '';
} catch { /* graceful failure */ }
if (result && result.trim()) {
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
additionalContext: result.trim()
}
}));
}
} catch (err) {
// Graceful failure — log to stderr for debugging
console.error('GitNexus hook error:', err.message);
}
}
main();
+304
View File
@@ -0,0 +1,304 @@
{
"agentPushNotifEnabled": true,
"enabledPlugins": {
"caveman@caveman": true,
"claude-mem@thedotmack": true,
"context-mode@context-mode": true,
"context7@claude-plugins-official": false,
"council@council-of-high-intelligence": true,
"exa@claude-plugins-official": true,
"frontend-design@claude-plugins-official": true,
"gopls-lsp@claude-plugins-official": true,
"gopls@claude-code-lsps": true,
"lua-lsp@claude-plugins-official": true,
"ponytail@ponytail": true,
"solidity-language-server@claude-code-lsps": true,
"understand-anything@understand-anything": true,
"vtsls@claude-code-lsps": true,
"yaml-lsp@zircote-lsp": true
},
"env": {
"CLAUDE_CODE_DISABLE_AUTO_MEMORY": "1",
"EDITOR": "/usr/bin/micro"
},
"extraKnownMarketplaces": {
"caveman": {
"source": {
"repo": "JuliusBrussee/caveman",
"source": "github"
}
},
"claude-code-lsps": {
"source": {
"repo": "Piebald-AI/claude-code-lsps",
"source": "github"
}
},
"claude-plugins-official": {
"source": {
"repo": "anthropics/claude-plugins-official",
"source": "github"
}
},
"council-of-high-intelligence": {
"source": {
"repo": "0xNyk/council-of-high-intelligence",
"source": "github"
}
},
"mempalace": {
"source": {
"repo": "MemPalace/mempalace",
"source": "github"
}
},
"omc": {
"source": {
"source": "git",
"url": "https://github.com/Yeachan-Heo/oh-my-claudecode.git"
}
},
"ponytail": {
"source": {
"repo": "DietrichGebert/ponytail",
"source": "github"
}
},
"thedotmack": {
"source": {
"repo": "thedotmack/claude-mem",
"source": "github"
}
},
"understand-anything": {
"source": {
"repo": "Egonex-AI/Understand-Anything",
"source": "github"
}
},
"zircote-lsp": {
"source": {
"repo": "zircote/lsp-marketplace",
"source": "github"
}
}
},
"hooks": {
"Notification": [
{
"_pixtuoid": true,
"hooks": [
{
"command": "pixtuoid-hook",
"type": "command"
}
],
"matcher": ".*"
}
],
"PermissionRequest": [
{
"hooks": [
{
"command": "if [ -x '/home/prad/.orca/agent-hooks/claude-hook.sh' ]; then /bin/sh '/home/prad/.orca/agent-hooks/claude-hook.sh'; fi",
"type": "command"
}
],
"matcher": "*"
}
],
"PostToolUse": [
{
"hooks": [
{
"command": "if [ -x '/home/prad/.orca/agent-hooks/claude-hook.sh' ]; then /bin/sh '/home/prad/.orca/agent-hooks/claude-hook.sh'; fi",
"type": "command"
}
],
"matcher": "*"
},
{
"_pixtuoid": true,
"hooks": [
{
"command": "pixtuoid-hook",
"type": "command"
}
],
"matcher": ".*"
},
{
"hooks": [
{
"command": "atuin hook claude-code",
"type": "command"
}
],
"matcher": "Bash"
}
],
"PostToolUseFailure": [
{
"hooks": [
{
"command": "if [ -x '/home/prad/.orca/agent-hooks/claude-hook.sh' ]; then /bin/sh '/home/prad/.orca/agent-hooks/claude-hook.sh'; fi",
"type": "command"
}
],
"matcher": "*"
},
{
"hooks": [
{
"command": "atuin hook claude-code",
"type": "command"
}
],
"matcher": "Bash"
}
],
"PreToolUse": [
{
"hooks": [
{
"command": "if [ -x '/home/prad/.orca/agent-hooks/claude-hook.sh' ]; then /bin/sh '/home/prad/.orca/agent-hooks/claude-hook.sh'; fi",
"type": "command"
}
],
"matcher": "*"
},
{
"_pixtuoid": true,
"hooks": [
{
"command": "pixtuoid-hook",
"type": "command"
}
],
"matcher": ".*"
},
{
"hooks": [
{
"command": "atuin hook claude-code",
"type": "command"
}
],
"matcher": "Bash"
}
],
"SessionEnd": [
{
"hooks": [
{
"command": "python3 '/home/prad/.claude/hooks/cmem-to-obsidian.py'",
"timeout": 15,
"type": "command"
}
]
},
{
"_pixtuoid": true,
"hooks": [
{
"command": "pixtuoid-hook",
"type": "command"
}
],
"matcher": ".*"
}
],
"SessionStart": [
{
"_pixtuoid": true,
"hooks": [
{
"command": "pixtuoid-hook",
"type": "command"
}
],
"matcher": ".*"
},
{
"hooks": [
{
"command": "bash '/home/prad/.claude/hooks/herdr-agent-state.sh' session",
"timeout": 10,
"type": "command"
}
],
"matcher": "*"
}
],
"Stop": [
{
"hooks": [
{
"command": "if [ -x '/home/prad/.orca/agent-hooks/claude-hook.sh' ]; then /bin/sh '/home/prad/.orca/agent-hooks/claude-hook.sh'; fi",
"type": "command"
}
]
}
],
"StopFailure": [
{
"hooks": [
{
"command": "if [ -x '/home/prad/.orca/agent-hooks/claude-hook.sh' ]; then /bin/sh '/home/prad/.orca/agent-hooks/claude-hook.sh'; fi",
"type": "command"
}
]
}
],
"SubagentStart": [
{
"_pixtuoid": true,
"hooks": [
{
"command": "pixtuoid-hook",
"type": "command"
}
],
"matcher": ".*"
}
],
"SubagentStop": [
{
"_pixtuoid": true,
"hooks": [
{
"command": "pixtuoid-hook",
"type": "command"
}
],
"matcher": ".*"
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"command": "if [ -x '/home/prad/.orca/agent-hooks/claude-hook.sh' ]; then /bin/sh '/home/prad/.orca/agent-hooks/claude-hook.sh'; fi",
"type": "command"
}
]
}
]
},
"model": "claude-fable-5",
"permissions": {
"allow": [
"mcp__codegraph__codegraph_search",
"mcp__codegraph__codegraph_context",
"mcp__codegraph__codegraph_callers",
"mcp__codegraph__codegraph_callees",
"mcp__codegraph__codegraph_impact",
"mcp__codegraph__codegraph_node",
"mcp__codegraph__codegraph_status"
],
"defaultMode": "bypassPermissions"
},
"skipAutoPermissionPrompt": true,
"skipDangerousModePermissionPrompt": true,
"tui": "fullscreen"
}
+256
View File
@@ -0,0 +1,256 @@
---
name: absurd
description: Debug and operate Absurd durable workflows with absurdctl. Use when working with Absurd queues, tasks, runs, events, retries, schema setup, or when a user wants to inspect, spawn, retry, cancel, or wake workflows.
license: Apache-2.0
---
# Absurd
Use this skill when the project uses **Absurd**, the Postgres-native durable workflow engine, or when the user mentions **`absurdctl`**, queues, durable tasks, runs, retries, sleeping tasks, or events.
## Tiny mental model
- A **queue** is a namespace of Absurd tables (`t_`, `r_`, `c_`, `e_`, `w_`).
- A **task** is the durable workflow instance.
- A **run** is one execution attempt of a task.
- A **step** is a checkpoint. Completed step results are stored as JSON.
- **Sleeping** tasks are usually waiting for time or an event.
- **Events** wake waiting tasks. Event payloads are cached; first emit wins.
Important distinction:
- `task_id` = the whole workflow across all attempts
- `run_id` = one specific execution attempt
## First: make sure `absurdctl` works
If `absurdctl` is not on `PATH`, check whether you are inside a repo checkout and, if so, use:
```bash
export PATH="$PWD:$PATH"
```
Absurd connection precedence is:
```text
--database > ABSURD_DATABASE_URL > PGDATABASE > postgresql://localhost/absurd
```
For non-URI connections, `PGHOST`, `PGPORT`, `PGUSER`, and `PGPASSWORD` are also honored.
## Default debugging workflow
Prefer **`absurdctl` state inspection before source inspection**. Usually you do **not** need to read application code first.
If the user explicitly asks you to use **`absurdctl`** to inspect or fix a workflow, do that first instead of starting with `rg` / source browsing.
When the user wants to debug a task, start with these commands in order:
### 1) Discover queues
```bash
absurdctl list-queues
```
### 2) Inspect recent activity in the likely queue
```bash
absurdctl list-tasks --queue=default --limit=20
```
Notes:
- `list-tasks` defaults to 50 rows if `--limit` is omitted.
- Useful statuses: `pending`, `running`, `sleeping`, `completed`, `failed`, `cancelled`.
### 3) Focus on failures or sleepers
```bash
absurdctl list-tasks --queue=default --status=failed --limit=20
absurdctl list-tasks --queue=default --status=sleeping --limit=20
```
### 4) Inspect one workflow or one attempt in detail
```bash
absurdctl dump-task --task-id=<task-id>
absurdctl dump-task --run-id=<run-id>
```
`dump-task` is the most important inspection command. It shows things like:
- task name, params, and headers
- retry settings and attempts
- checkpointed step state
- waits / events / sleep state
- final result or failure
## How to reason about common states
### If a task is `failed`
1. `dump-task --task-id=<task-id>`
2. Read the failure and the last successful checkpoints.
3. If needed, inspect the most recent attempt with `--run-id`.
4. Search the code for the task implementation by task name.
5. Only then decide whether to retry.
### If a task is `sleeping`
1. `dump-task --task-id=<task-id>`
2. Look for the wait reason:
- sleeping until a timestamp
- waiting for an event name
3. If it is waiting for an event and the user wants it resumed, emit that event.
### If a task is `running`
1. `dump-task --task-id=<task-id>`
2. Look at existing checkpoints to see how far it got.
3. If the user suspects a stuck worker, inspect the worker process / application logs too.
### About workers
Do not assume you need to start or modify a worker.
- If a spawned task moves from `pending` to `sleeping`, `running`, or `completed`, a worker is already active.
- If tasks remain `pending`, then investigate whether a worker for that queue is actually running.
- Only inspect Python / TypeScript runtime details when the task state suggests a worker problem or the user asks for code changes.
### If you need the implementation
After you know the task name, search the codebase for its registration.
TypeScript / JavaScript:
```bash
rg -n "registerTask\(|name:\s*['\"]<task-name>['\"]" .
```
Python:
```bash
rg -n "register_task\(|@.*register_task|['\"]<task-name>['\"]" .
```
If the task is waiting for an event, also search for the event name.
## Common actions
### Spawn work
Use `-P key=value` for strings and `-P key:=json` for typed JSON values.
```bash
absurdctl spawn-task my-task -q default -P foo=bar
absurdctl spawn-task my-task -q default -P count:=42 -P enabled:=true
absurdctl spawn-task my-task -q default -P user.name=Alice -P user.age:=30
```
Use `--params` when the user already has a JSON object:
```bash
absurdctl spawn-task my-task -q default --params '{"foo":"bar","count":42}'
```
### Retry failed work
```bash
absurdctl retry-task <task-id>
absurdctl retry-task <task-id> --max-attempts 5
absurdctl retry-task -q default <task-id> --spawn-new
```
Guidance:
- plain `retry-task` retries the existing task
- `--spawn-new` creates a brand-new task with the original inputs
- prefer understanding the failure before retrying
### Cancel work
```bash
absurdctl cancel-task <task-id>
absurdctl cancel-task -q default <task-id>
```
### Wake waiting tasks by emitting an event
```bash
absurdctl emit-event order.completed -q default -P orderId=123
absurdctl emit-event approval.granted:42 -q default -P approved:=true
```
If the event payload should be structured JSON:
```bash
absurdctl emit-event shipment.packed:42 -q default --payload '{"trackingNumber":"XYZ"}'
```
### Schema setup / migrations
Use these on a blank or controlled database, or when the user explicitly asks:
```bash
absurdctl init
absurdctl schema-version
absurdctl migrate
absurdctl create-queue default
```
## Safe operating rules
Be careful with state-changing commands. Unless the user clearly wants them, avoid running these blindly on a shared or production database:
- `init`
- `migrate`
- `create-queue`
- `drop-queue`
- `cleanup`
- `cancel-task`
- `retry-task`
- `emit-event`
- `spawn-task`
If the environment is ambiguous, ask which database / queue is safe to operate on.
## Good copy-paste playbooks
### Debug the latest failures in `default`
```bash
absurdctl list-queues
absurdctl list-tasks --queue=default --status=failed --limit=20
absurdctl dump-task --task-id=<task-id>
```
### Find sleepers and wake one
```bash
absurdctl list-tasks --queue=default --status=sleeping --limit=20
absurdctl dump-task --task-id=<task-id>
absurdctl emit-event <event-name> -q default -P key=value
```
### Reproduce by spawning a task, then inspect it
```bash
absurdctl spawn-task my-task -q default -P foo=bar
absurdctl list-tasks --queue=default --task-name=my-task --limit=5
absurdctl dump-task --task-id=<task-id>
```
Fast path when the user says “spawn a task and debug it”:
```bash
absurdctl spawn-task my-task -q default -P foo=bar
absurdctl list-tasks --queue=default --task-name=my-task --limit=5
absurdctl dump-task --task-id=<task-id>
# then either:
absurdctl emit-event <event-name> -q default -P key=value
# or:
absurdctl retry-task <task-id>
```
## Extra reference
- Use `absurdctl <command> --help` for full options.
- `dump-task --task-id` is usually the best starting point once you know the task.
- Checkpointed step results are durable JSON state; code outside steps may execute multiple times across retries.
+221
View File
@@ -0,0 +1,221 @@
---
name: agents-sdk
description: Build AI agents on Cloudflare Workers using the Agents SDK. Load when creating stateful agents, durable workflows, real-time WebSocket apps, scheduled tasks, MCP servers, chat applications, voice agents, or browser automation. Covers Agent class, state management, callable RPC, Workflows, durable execution, queues, retries, observability, and React hooks. Biases towards retrieval from Cloudflare docs over pre-trained knowledge.
---
# Cloudflare Agents SDK
Your knowledge of the Agents SDK may be outdated. **Prefer retrieval over pre-training** for any Agents SDK task.
## Retrieval Sources
Cloudflare docs: https://developers.cloudflare.com/agents/
| Topic | Docs URL | Use for |
|-------|----------|---------|
| Getting started | [Quick start](https://developers.cloudflare.com/agents/getting-started/quick-start/) | First agent, project setup |
| Adding to existing project | [Add to existing project](https://developers.cloudflare.com/agents/getting-started/add-to-existing-project/) | Install into existing Workers app |
| Configuration | [Configuration](https://developers.cloudflare.com/agents/api-reference/configuration/) | `wrangler.jsonc`, bindings, assets, deployment |
| Agent class | [Agents API](https://developers.cloudflare.com/agents/api-reference/agents-api/) | Agent lifecycle, patterns, pitfalls |
| State | [Store and sync state](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) | `setState`, `validateStateChange`, persistence |
| Routing | [Routing](https://developers.cloudflare.com/agents/api-reference/routing/) | URL patterns, `routeAgentRequest` |
| Callable methods | [Callable methods](https://developers.cloudflare.com/agents/api-reference/callable-methods/) | `@callable`, RPC, streaming, timeouts |
| Scheduling | [Schedule tasks](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) | `schedule()`, `scheduleEvery()`, cron |
| Workflows | [Run workflows](https://developers.cloudflare.com/agents/api-reference/run-workflows/) | `AgentWorkflow`, durable multi-step tasks |
| HTTP/WebSockets | [WebSockets](https://developers.cloudflare.com/agents/api-reference/websockets/) | Lifecycle hooks, hibernation |
| Chat agents | [Chat agents](https://developers.cloudflare.com/agents/api-reference/chat-agents/) | `AIChatAgent`, streaming, tools, persistence |
| Client SDK | [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/) | `useAgent`, `useAgentChat`, React hooks |
| Client tools | [Client tools](https://developers.cloudflare.com/agents/api-reference/client-tools/) | Client-side tools, `autoContinueAfterToolResult` |
| Server-driven messages | [Trigger patterns](https://developers.cloudflare.com/agents/api-reference/trigger-patterns/) | `saveMessages`, `waitUntilStable`, server-initiated turns |
| Resumable streaming | [Resumable streaming](https://developers.cloudflare.com/agents/api-reference/resumable-streaming/) | Stream recovery on disconnect |
| Email | [Email](https://developers.cloudflare.com/agents/api-reference/email/) | Email routing, secure reply resolver |
| MCP client | [MCP client](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/) | Connecting to MCP servers |
| MCP server | [MCP server](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/) | Building MCP servers with `McpAgent` |
| MCP transports | [MCP transports](https://developers.cloudflare.com/agents/api-reference/mcp-transports/) | Streamable HTTP, SSE, RPC transport options |
| Securing MCP servers | [Securing MCP](https://developers.cloudflare.com/agents/api-reference/securing-mcp-servers/) | OAuth, proxy MCP, hardening |
| Human-in-the-loop | [Human-in-the-loop](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/) | Approval flows, `needsApproval`, workflows |
| Durable execution | [Durable execution](https://developers.cloudflare.com/agents/api-reference/durable-execution/) | `runFiber()`, `stash()`, surviving DO eviction |
| Queue | [Queue](https://developers.cloudflare.com/agents/api-reference/queue-tasks/) | Built-in FIFO queue, `queue()` |
| Retries | [Retries](https://developers.cloudflare.com/agents/api-reference/retries/) | `this.retry()`, backoff/jitter |
| Observability | [Observability](https://developers.cloudflare.com/agents/api-reference/observability/) | Diagnostics-channel events |
| Push notifications | [Push notifications](https://developers.cloudflare.com/agents/api-reference/push-notifications/) | Web Push + VAPID from agents |
| Webhooks | [Webhooks](https://developers.cloudflare.com/agents/api-reference/webhooks/) | Receiving external webhooks |
| Cross-domain auth | [Cross-domain auth](https://developers.cloudflare.com/agents/api-reference/cross-domain-authentication/) | WebSocket auth, tokens, CORS |
| Readonly connections | [Readonly](https://developers.cloudflare.com/agents/api-reference/readonly-connections/) | `shouldConnectionBeReadonly` |
| Voice | [Voice](https://developers.cloudflare.com/agents/api-reference/voice/) | Experimental STT/TTS, `withVoice` |
| Browse the web | [Browser tools](https://developers.cloudflare.com/agents/api-reference/browse-the-web/) | Experimental CDP browser automation |
| Think | [Think](https://developers.cloudflare.com/agents/api-reference/think/) | Experimental higher-level chat agent class |
| Migrations | [AI SDK v5](https://developers.cloudflare.com/agents/guides/migration-to-ai-sdk-v5/), [AI SDK v6](https://developers.cloudflare.com/agents/guides/migration-to-ai-sdk-v6/) | Upgrading `@cloudflare/ai-chat` |
## Capabilities
The Agents SDK provides:
- **Persistent state** — SQLite-backed, auto-synced to clients via `setState`
- **Callable RPC** — `@callable()` methods invoked over WebSocket
- **Scheduling** — One-time, recurring (`scheduleEvery`), and cron tasks
- **Workflows** — Durable multi-step background processing via `AgentWorkflow`
- **Durable execution** — `runFiber()` / `stash()` for work that survives DO eviction
- **Queue** — Built-in FIFO queue with retries via `queue()`
- **Retries** — `this.retry()` with exponential backoff and jitter
- **MCP integration** — Connect to MCP servers or build your own with `McpAgent`
- **Email handling** — Receive and reply to emails with secure routing
- **Streaming chat** — `AIChatAgent` with resumable streams, message persistence, tools
- **Server-driven messages** — `saveMessages`, `waitUntilStable` for proactive agent turns
- **React hooks** — `useAgent`, `useAgentChat` for client apps
- **Observability** — `diagnostics_channel` events for state, RPC, schedule, lifecycle
- **Push notifications** — Web Push + VAPID delivery from agents
- **Webhooks** — Receive and verify external webhooks
- **Voice** (experimental) — STT/TTS via `@cloudflare/voice`
- **Browser tools** (experimental) — CDP-powered browsing via `agents/browser`
- **Think** (experimental) — Higher-level chat agent via `@cloudflare/think`
## FIRST: Verify Installation
```bash
npm ls agents # Should show agents package
```
If not installed:
```bash
npm install agents
```
For chat agents:
```bash
npm install agents @cloudflare/ai-chat ai @ai-sdk/react
```
## Wrangler Configuration
```jsonc
{
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }]
}
```
**Gotchas:**
- Do NOT enable `experimentalDecorators` in tsconfig (breaks `@callable`)
- Never edit old migrations — always add new tags
- Each agent class needs its own DO binding + migration entry
- Add `"ai": { "binding": "AI" }` for Workers AI
## Agent Class
```typescript
import { Agent, routeAgentRequest, callable } from "agents";
type State = { count: number };
export class Counter extends Agent<Env, State> {
initialState = { count: 0 };
validateStateChange(nextState: State, source: Connection | "server") {
if (nextState.count < 0) throw new Error("Count cannot be negative");
}
onStateUpdate(state: State, source: Connection | "server") {
console.log("State updated:", state);
}
@callable()
increment() {
this.setState({ count: this.state.count + 1 });
return this.state.count;
}
}
export default {
fetch: (req, env) => routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 })
};
```
## Routing
Requests route to `/agents/{agent-name}/{instance-name}`:
| Class | URL |
|-------|-----|
| `Counter` | `/agents/counter/user-123` |
| `ChatRoom` | `/agents/chat-room/lobby` |
Client: `useAgent({ agent: "Counter", name: "user-123" })`
Custom routing: use `getAgentByName(env.MyAgent, "instance-id")` then `agent.fetch(request)`.
## Core APIs
| Task | API |
|------|-----|
| Read state | `this.state.count` |
| Write state | `this.setState({ count: 1 })` |
| SQL query | `` this.sql`SELECT * FROM users WHERE id = ${id}` `` |
| Schedule (delay) | `await this.schedule(60, "task", payload)` |
| Schedule (cron) | `await this.schedule("0 * * * *", "task", payload)` |
| Schedule (interval) | `await this.scheduleEvery(30, "poll")` |
| RPC method | `@callable() myMethod() { ... }` |
| Streaming RPC | `@callable({ streaming: true }) stream(res) { ... }` |
| Start workflow | `await this.runWorkflow("ProcessingWorkflow", params)` |
| Durable fiber | `await this.runFiber("name", async (ctx) => { ... })` |
| Enqueue work | `this.queue("handler", payload)` |
| Retry with backoff | `await this.retry(fn, { maxAttempts: 5 })` |
| Broadcast to clients | `this.broadcast(message)` |
| Get connections | `this.getConnections(tag?)` |
## React Client
```tsx
import { useAgent } from "agents/react";
function App() {
const [state, setLocalState] = useState({ count: 0 });
const agent = useAgent({
agent: "Counter",
name: "my-instance",
onStateUpdate: (newState) => setLocalState(newState),
onIdentity: (name, agentType) => console.log(`Connected to ${name}`)
});
return (
<button onClick={() => agent.setState({ count: state.count + 1 })}>
Count: {state.count}
</button>
);
}
```
## References
### Core
- **[references/state-scheduling.md](references/state-scheduling.md)** — State persistence, scheduling, SQL
- **[references/callable.md](references/callable.md)** — RPC methods, streaming, timeouts
- **[references/routing.md](references/routing.md)** — URL patterns, custom routing, `getAgentByName`
- **[references/configuration.md](references/configuration.md)** — Wrangler config, bindings, Vite setup
### Chat & Streaming
- **[references/streaming-chat.md](references/streaming-chat.md)** — AIChatAgent, resumable streams, tools
- **[references/client-sdk.md](references/client-sdk.md)** — `useAgent`, `useAgentChat`, `AgentClient`
- **[references/server-driven-messages.md](references/server-driven-messages.md)** — Trigger patterns, `saveMessages`
- **[references/human-in-the-loop.md](references/human-in-the-loop.md)** — Approval flows, `needsApproval`
### Background Processing
- **[references/workflows.md](references/workflows.md)** — Durable Workflows integration
- **[references/durable-execution.md](references/durable-execution.md)** — `runFiber`, `stash`, surviving eviction
- **[references/queue-retries.md](references/queue-retries.md)** — Built-in queue, retry with backoff
### Integrations
- **[references/mcp.md](references/mcp.md)** — MCP client and server, transports, securing
- **[references/email.md](references/email.md)** — Email routing and handling
- **[references/webhooks-push.md](references/webhooks-push.md)** — Webhooks, push notifications
- **[references/observability.md](references/observability.md)** — Diagnostics-channel events
### Experimental
- **[references/think.md](references/think.md)** — `@cloudflare/think` higher-level chat agent
- **[references/voice.md](references/voice.md)** — `@cloudflare/voice` STT/TTS
- **[references/codemode.md](references/codemode.md)** — Code Mode for tool orchestration
- **[references/browse-the-web.md](references/browse-the-web.md)** — CDP browser tools
@@ -0,0 +1,63 @@
# Browse the Web (Experimental)
Fetch https://developers.cloudflare.com/agents/api-reference/browse-the-web/ for complete documentation.
CDP-powered browser tools that let agents scrape, screenshot, and interact with web pages.
## Setup
```jsonc
// wrangler.jsonc
{
"browser": { "binding": "BROWSER" },
"worker_loaders": [{ "binding": "LOADER" }],
"compatibility_flags": ["nodejs_compat"]
}
```
## Usage with AI SDK
```typescript
import { createBrowserTools } from "agents/browser/ai";
export class MyAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const browserTools = createBrowserTools({
browser: this.env.BROWSER,
loader: this.env.LOADER
});
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
tools: { ...myTools, ...browserTools },
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## Available Tools
| Tool | Purpose |
|------|---------|
| `browser_search` | Search the web and return results |
| `browser_execute` | Navigate to URL, execute JS, return results |
The LLM writes async JavaScript IIFEs that run in a fresh browser session.
## When to Use
- Need a real browser (JS rendering, screenshots, interaction) → browser tools
- Just need HTML/API data → use `fetch()` instead (faster, cheaper)
## Low-Level API
```typescript
import { connectBrowser, CdpSession } from "agents/browser";
const browser = await connectBrowser(this.env.BROWSER);
const cdp = new CdpSession(browser);
await cdp.send("Page.navigate", { url: "https://example.com" });
```
@@ -0,0 +1,92 @@
# Callable Methods
Fetch https://developers.cloudflare.com/agents/api-reference/callable-methods/ for complete documentation.
## Overview
`@callable()` exposes agent methods to clients via WebSocket RPC.
```typescript
import { Agent, callable } from "agents";
export class MyAgent extends Agent<Env, State> {
@callable()
async greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
@callable()
async processData(data: unknown): Promise<Result> {
// Long-running work
return result;
}
}
```
## Client Usage
```typescript
// Basic call
const greeting = await agent.call("greet", ["World"]);
// With timeout
const result = await agent.call("processData", [data], {
timeout: 5000 // 5 second timeout
});
```
## Streaming Responses
```typescript
import { Agent, callable, StreamingResponse } from "agents";
export class MyAgent extends Agent<Env, State> {
@callable({ streaming: true })
async streamResults(stream: StreamingResponse, query: string) {
for await (const item of fetchResults(query)) {
stream.send(JSON.stringify(item));
}
stream.close();
}
@callable({ streaming: true })
async streamWithError(stream: StreamingResponse) {
try {
// ... work
} catch (error) {
stream.error(error.message); // Signal error to client
return;
}
stream.close();
}
}
```
Client with streaming:
```typescript
await agent.call("streamResults", ["search term"], {
stream: {
onChunk: (data) => console.log("Chunk:", data),
onDone: () => console.log("Complete"),
onError: (error) => console.error("Error:", error)
}
});
```
## Introspection
```typescript
// Get list of callable methods on an agent
const methods = await agent.call("getCallableMethods", []);
// Returns: ["greet", "processData", "streamResults", ...]
```
## When to Use
| Scenario | Use |
|----------|-----|
| Browser/mobile calling agent | `@callable()` |
| External service calling agent | `@callable()` |
| Worker calling agent (same codebase) | DO RPC directly |
| Agent calling another agent | `getAgentByName()` + DO RPC |
@@ -0,0 +1,110 @@
# Client SDK
Fetch https://developers.cloudflare.com/agents/api-reference/client-sdk/ for complete documentation.
## React: `useAgent`
```tsx
import { useAgent } from "agents/react";
function App() {
const [state, setState] = useState({ count: 0 });
const agent = useAgent({
agent: "Counter",
name: "my-instance",
onStateUpdate: (newState) => setState(newState),
onIdentity: (name, agentType) => console.log(`Connected to ${name}`)
});
return <button onClick={() => agent.setState({ count: state.count + 1 })}>
{state.count}
</button>;
}
```
### Typed RPC via `stub`
```tsx
const agent = useAgent<typeof MyAgent>({
agent: "MyAgent",
name: "default"
});
const result = await agent.stub.myMethod(arg1, arg2);
```
### Auth via Query Params
```tsx
useAgent({
agent: "MyAgent",
name: "default",
query: async () => `token=${await getToken()}`,
queryDeps: [tokenVersion]
});
```
## React: `useAgentChat`
```tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent", name: "session-1" });
const { messages, input, handleInputChange, handleSubmit, status } =
useAgentChat({ agent });
return (
<div>
{messages.map((m) => <div key={m.id}>{m.role}: {m.content}</div>)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
</div>
);
}
```
## Vanilla JS: `AgentClient`
```typescript
import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "MyAgent",
name: "default",
host: "https://my-worker.workers.dev"
});
client.addEventListener("stateUpdate", (e) => console.log(e.state));
const result = await client.call("myMethod", [arg]);
client.close();
```
## `agentFetch` for HTTP-only
```typescript
import { agentFetch } from "agents/client";
const response = await agentFetch({
agent: "MyAgent",
name: "default",
host: "https://my-worker.workers.dev",
path: "/api/data"
});
```
## Streaming RPC
```typescript
await agent.call("streamResults", ["query"], {
stream: {
onChunk: (data) => console.log(data),
onDone: () => console.log("done"),
onError: (err) => console.error(err)
}
});
```
@@ -0,0 +1,110 @@
# Codemode (Experimental)
Fetch https://developers.cloudflare.com/agents/api-reference/codemode/ for complete documentation.
Codemode lets LLMs write and execute code that orchestrates your tools, instead of calling them one at a time. The LLM gets a single "write code" tool; generated JavaScript runs in an isolated Worker sandbox.
## When to Use
| Scenario | Use Codemode? |
|----------|---------------|
| Single tool call | No — standard tool calling is simpler |
| Chained tool calls with logic | Yes |
| Conditional logic across tools | Yes |
| MCP multi-server workflows | Yes |
| Simple Q&A chat | No |
## Setup
### Wrangler Config
```jsonc
{
"worker_loaders": [{ "binding": "LOADER" }],
"compatibility_flags": ["nodejs_compat"]
}
```
### Install
```bash
npm install @cloudflare/codemode ai zod
```
## Usage
```typescript
import { createCodeTool } from "@cloudflare/codemode/ai";
import { DynamicWorkerExecutor } from "@cloudflare/codemode";
import { streamText, tool, convertToModelMessages } from "ai";
import { z } from "zod";
const tools = {
getWeather: tool({
description: "Get weather for a location",
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => `Weather: ${location} 72°F`
}),
sendEmail: tool({
description: "Send an email",
inputSchema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
execute: async ({ to, subject, body }) => `Email sent to ${to}`
})
};
export class MyAgent extends Agent<Env, State> {
async onChatMessage() {
const executor = new DynamicWorkerExecutor({
loader: this.env.LOADER
});
const codemode = createCodeTool({ tools, executor });
const result = streamText({
model,
system: "You are a helpful assistant.",
messages: await convertToModelMessages(this.messages),
tools: { codemode }
});
return result.toUIMessageStreamResponse();
}
}
```
## With MCP Tools
```typescript
const codemode = createCodeTool({
tools: {
...myTools,
...this.mcp.getAITools()
},
executor
});
```
## How It Works
1. `createCodeTool` generates TypeScript type definitions from your tools
2. The LLM writes an async arrow function calling `codemode.toolName(args)`
3. Code runs in an isolated Worker sandbox via `DynamicWorkerExecutor`
4. Tool calls route back to the host via Workers RPC
5. External `fetch()` is blocked by default — sandbox can only call your tools
## Network Isolation
```typescript
const executor = new DynamicWorkerExecutor({
loader: env.LOADER,
globalOutbound: null // default — fully isolated
// globalOutbound: env.MY_SERVICE // route through a Fetcher
});
```
## Limitations
- Experimental — API may change
- `needsApproval` tools execute immediately in sandbox (no approval pause yet)
- JavaScript execution only
- Requires `worker_loaders` binding
@@ -0,0 +1,72 @@
# Configuration
Fetch https://developers.cloudflare.com/agents/api-reference/configuration/ for complete documentation.
## Wrangler Config (`wrangler.jsonc`)
```jsonc
{
"name": "my-agent",
"main": "src/index.ts",
"compatibility_date": "2025-01-28",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{ "name": "MyAgent", "class_name": "MyAgent" },
{ "name": "ChatAgent", "class_name": "ChatAgent" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyAgent", "ChatAgent"] }
],
"ai": { "binding": "AI" },
"assets": {
"directory": "./dist/client",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": true
}
}
```
## Key Rules
- Every agent class needs a DO binding AND a `new_sqlite_classes` migration entry
- `nodejs_compat` is required
- Never edit old migrations — add a new tag (e.g. `v2`) for new classes
- Do NOT enable `experimentalDecorators` in tsconfig — it breaks `@callable`
- For Workers AI locally, set `"ai": { "binding": "AI", "remote": true }` in `.dev.vars` or config
- Use `wrangler secret put` for secrets, never hardcode them
## Vite Setup
```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";
import { agents } from "agents/vite";
export default defineConfig({
plugins: [react(), cloudflare(), agents()]
});
```
## Type Generation
```bash
npx wrangler types
```
This generates `env.d.ts` with typed bindings. Regenerate after changing `wrangler.jsonc`.
## tsconfig
Extend the agents tsconfig for correct settings:
```jsonc
{
"extends": ["agents/tsconfig"],
"include": ["src/**/*.ts", "src/**/*.tsx"],
"compilerOptions": { "paths": { "~/*": ["./src/*"] } }
}
```
@@ -0,0 +1,51 @@
# Durable Execution
Fetch https://developers.cloudflare.com/agents/api-reference/durable-execution/ for complete documentation.
Fibers let agent work survive Durable Object eviction. Progress is checkpointed to SQLite; on recovery, you decide what to do.
## `runFiber`
```typescript
export class MyAgent extends Agent<Env, State> {
async onRequest(request: Request) {
await this.runFiber("process-data", async (ctx) => {
const step1 = await fetchData();
ctx.stash({ step: 1, data: step1 });
const step2 = await transform(step1);
ctx.stash({ step: 2, result: step2 });
this.setState({ result: step2 });
});
return new Response("Started");
}
async onFiberRecovered(ctx) {
const checkpoint = ctx.stash;
if (checkpoint.step === 1) {
const step2 = await transform(checkpoint.data);
this.setState({ result: step2 });
}
}
}
```
## Key APIs
| API | Purpose |
|-----|---------|
| `this.runFiber(name, fn)` | Start a named fiber |
| `ctx.stash` / `this.stash` | Read latest checkpoint |
| `ctx.stash = data` | Write checkpoint (JSON-serializable) |
| `onFiberRecovered(ctx)` | Called on DO restart if fiber was in-flight |
| `keepAlive()` | Prevent hibernation while fiber runs |
| `keepAliveWhile(fn)` | Keep alive for duration of async function |
## Important
- `stash` replaces the entire checkpoint — not a merge
- The lambda is NOT restored on recovery — only the stash data is. You must re-derive what to do in `onFiberRecovered`
- No auto-retry on throw — handle errors yourself
- For long-running pipelines with automatic retries, use Workflows instead
- Filter concurrent fibers by `ctx.name` in `onFiberRecovered`
@@ -0,0 +1,146 @@
# Email Handling
Fetch https://developers.cloudflare.com/agents/api-reference/email/ for complete documentation.
## Overview
Agents receive and reply to emails via Cloudflare Email Routing.
## Wrangler Configuration
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "EmailAgent", "class_name": "EmailAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["EmailAgent"] }],
"send_email": [
{ "name": "SEB", "destination_address": "reply@yourdomain.com" }
]
}
```
## Basic Email Handler
```typescript
import { Agent } from "agents";
import { type AgentEmail } from "agents/email";
import PostalMime from "postal-mime";
export class EmailAgent extends Agent<Env, State> {
async onEmail(email: AgentEmail) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
console.log("From:", email.from);
console.log("Subject:", parsed.subject);
await this.replyToEmail(email, {
fromName: "My Agent",
subject: `Re: ${parsed.subject}`,
body: "Thanks for your email!"
});
}
}
```
## Routing Emails
```typescript
import { routeAgentRequest, routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
export default {
async email(message, env) {
await routeAgentEmail(message, env, {
resolver: createAddressBasedEmailResolver("EmailAgent")
});
},
async fetch(request, env) {
return routeAgentRequest(request, env) ?? new Response("Not found", { status: 404 });
}
};
```
## Resolvers
### Address-Based (Inbound Mail)
Routes based on recipient address:
```typescript
import { createAddressBasedEmailResolver } from "agents/email";
const resolver = createAddressBasedEmailResolver("EmailAgent");
// support@example.com → EmailAgent, instance "support"
// NotificationAgent+user123@example.com → NotificationAgent, instance "user123"
```
### Secure Reply (Reply Flows)
Verifies replies are authentic using HMAC-SHA256 signatures:
```typescript
import { createSecureReplyEmailResolver } from "agents/email";
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
maxAge: 7 * 24 * 60 * 60, // 7 days (default: 30 days)
onInvalidSignature: (email, reason) => {
console.warn(`Invalid signature from ${email.from}: ${reason}`);
}
});
```
Sign outbound emails to enable secure reply routing:
```typescript
await this.replyToEmail(email, {
fromName: "My Agent",
body: "Thanks!",
secret: this.env.EMAIL_SECRET // Signs headers for secure reply routing
});
```
### Catch-All (Single Instance)
Routes all emails to one agent instance:
```typescript
import { createCatchAllEmailResolver } from "agents/email";
const resolver = createCatchAllEmailResolver("EmailAgent", "default");
```
### Combining Resolvers
```typescript
async email(message, env) {
const secureReply = createSecureReplyEmailResolver(env.EMAIL_SECRET);
const addressBased = createAddressBasedEmailResolver("EmailAgent");
await routeAgentEmail(message, env, {
resolver: async (email, env) => {
// Try secure reply first
const result = await secureReply(email, env);
if (result) return result;
// Fall back to address-based
return addressBased(email, env);
}
});
}
```
## Utilities
```typescript
import { isAutoReplyEmail } from "agents/email";
async onEmail(email: AgentEmail) {
if (isAutoReplyEmail(email.headers)) {
// Skip auto-replies (vacation, out-of-office, etc.)
return;
}
// Process email...
}
```
@@ -0,0 +1,67 @@
# Human-in-the-Loop
Fetch https://developers.cloudflare.com/agents/concepts/human-in-the-loop/ for complete documentation.
Multiple patterns for adding human approval to agent actions.
## Decision Guide
| Pattern | Best for |
|---------|----------|
| Workflows `waitForApproval` | Long-running background tasks |
| AI SDK `needsApproval` on tools | Chat tool calls requiring approval |
| Client tools (`onToolCall`) | Tools that execute in the browser |
| MCP `elicitInput` | Gathering structured input from MCP clients |
## Workflow Approvals
```typescript
// In AgentWorkflow:
const approved = await step.waitForEvent<{ approved: boolean }>("approval", {
timeout: "7d"
});
if (!approved.approved) throw new Error("Rejected");
// From agent:
await this.approveWorkflow(workflowId);
await this.rejectWorkflow(workflowId);
```
## Chat Tool Approvals (`needsApproval`)
```typescript
const tools = {
deleteItem: tool({
description: "Delete an item",
parameters: z.object({ id: z.string() }),
execute: async ({ id }) => { /* delete */ },
needsApproval: true // or a function: (toolCall) => boolean
})
};
```
Client handles approval:
```tsx
const { addToolApprovalResponse, addToolOutput } = useAgentChat({
agent,
onToolCall: async ({ toolCall }) => {
if (confirm(`Allow ${toolCall.toolName}?`)) {
return { approve: true };
}
return { approve: false };
}
});
```
To deny with a custom message:
```tsx
addToolOutput(toolCallId, "output-error", "User rejected this action");
```
## Important
- `waitForApproval` may return `undefined` on timeout — handle it
- `addToolOutput` with `output-error` does NOT auto-continue the LLM — you may need `sendMessage` after
- For OpenAI Agents SDK, use `needsApproval` on the tool definition (same pattern)
@@ -0,0 +1,188 @@
# MCP Integration
Fetch https://developers.cloudflare.com/agents/api-reference/mcp-client-api/ and https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/ for complete documentation.
Agents include a multi-server MCP client for connecting to external MCP servers, and `McpAgent` for building MCP servers.
## Add an MCP Server
```typescript
import { Agent, callable } from "agents";
export class MyAgent extends Agent<Env, State> {
@callable()
async addServer(name: string, url: string) {
// Options-based API (recommended)
const result = await this.addMcpServer(name, url, {
callbackHost: "https://my-worker.workers.dev",
transport: { headers: { Authorization: "Bearer ..." } }
});
if (result.state === "authenticating") {
// OAuth required - redirect user to result.authUrl
return { needsAuth: true, authUrl: result.authUrl };
}
return { ready: true, id: result.id };
}
}
```
## Use MCP Tools
```typescript
async onChatMessage() {
// Get AI-compatible tools from all connected MCP servers
const mcpTools = this.mcp.getAITools();
const allTools = {
...localTools,
...mcpTools
};
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
tools: allTools
});
return result.toUIMessageStreamResponse();
}
```
## List MCP Resources
```typescript
// List all registered servers
const servers = this.mcp.listServers();
// List tools from all servers
const tools = this.mcp.listTools();
// List resources
const resources = this.mcp.listResources();
// List prompts
const prompts = this.mcp.listPrompts();
```
## Remove Server
```typescript
await this.removeMcpServer(serverId);
```
## Building an MCP Server
Use `McpAgent` from the SDK to create an MCP server.
**Install dependencies:**
```bash
npm install @modelcontextprotocol/sdk zod
```
**Wrangler config:**
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "MyMCP", "class_name": "MyMCP" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyMCP"] }]
}
```
**Server implementation:**
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import { z } from "zod";
type State = { counter: number };
export class MyMCP extends McpAgent<Env, State, {}> {
server = new McpServer({
name: "MyMCPServer",
version: "1.0.0"
});
initialState = { counter: 0 };
async init() {
// Register a resource
this.server.resource("counter", "mcp://resource/counter", (uri) => ({
contents: [{ text: String(this.state.counter), uri: uri.href }]
}));
// Register a tool
this.server.registerTool(
"increment",
{
description: "Increment the counter",
inputSchema: { amount: z.number().default(1) }
},
async ({ amount }) => {
this.setState({ counter: this.state.counter + amount });
return {
content: [{ text: `Counter: ${this.state.counter}`, type: "text" }]
};
}
);
}
}
```
## Serve MCP Server
```typescript
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
// Streamable HTTP transport (recommended)
if (url.pathname.startsWith("/mcp")) {
return MyMCP.serve("/mcp", { binding: "MyMCP" }).fetch(request, env, ctx);
}
// SSE transport (legacy, deprecated)
if (url.pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse", { binding: "MyMCP" }).fetch(request, env, ctx);
}
return new Response("Not found", { status: 404 });
}
};
```
## Transports
Fetch https://developers.cloudflare.com/agents/api-reference/mcp-transports/ for complete documentation.
| Transport | Use for |
|-----------|---------|
| Streamable HTTP (`serve`) | External/public clients (recommended) |
| SSE (`serveSSE`) | Legacy clients only (deprecated) |
| RPC (`addMcpServer(name, env.Binding)`) | Same-Worker internal calls (fastest) |
### RPC Transport (Same Worker)
```typescript
async onStart() {
await this.addMcpServer("internal-tools", this.env.MyMCPBinding, {
props: { userId: this.name }
});
}
```
## Retry on MCP Connections
```typescript
await this.addMcpServer("tools", url, {
retry: { maxAttempts: 3, baseDelayMs: 500 }
});
```
## Securing MCP Servers
Fetch https://developers.cloudflare.com/agents/api-reference/securing-mcp-servers/ for complete documentation.
Use `@cloudflare/workers-oauth-provider` to add OAuth in front of your MCP server. See the securing docs for proxy patterns and `redirect_uri` validation.
@@ -0,0 +1,44 @@
# Observability
Fetch https://developers.cloudflare.com/agents/api-reference/observability/ for complete documentation.
Agents emit structured events via Node.js `diagnostics_channel`. Subscribe in development or forward via Tail Workers in production.
## Subscribe to Events
```typescript
import { subscribe } from "agents/observability";
subscribe("agents:rpc", (event) => {
console.log(`RPC call: ${event.payload.method}`);
});
subscribe("agents:state", (event) => {
console.log(`State change on ${event.agent}`);
});
```
## Available Channels
| Channel | Events |
|---------|--------|
| `agents:state` | State changes |
| `agents:rpc` | `@callable` invocations |
| `agents:message` | WebSocket messages |
| `agents:schedule` | Schedule triggers |
| `agents:lifecycle` | Agent start, connect, disconnect |
| `agents:workflow` | Workflow progress, completion, errors |
| `agents:mcp` | MCP server connections, tool calls |
| `agents:email` | Email received |
## Per-Agent Override
```typescript
export class MyAgent extends Agent<Env, State> {
observability = undefined; // disable for this agent
}
```
## Production: Tail Workers
In production, events appear as `diagnosticsChannelEvents` on the Tail Worker `event` object. Attach a Tail Worker to your agent's Worker to forward events to your observability platform.
@@ -0,0 +1,79 @@
# Queue & Retries
Fetch https://developers.cloudflare.com/agents/api-reference/queue-tasks/ and https://developers.cloudflare.com/agents/api-reference/retries/ for complete documentation.
## Built-in Queue
FIFO queue persisted in SQLite. Sequential processing, one item at a time.
```typescript
export class MyAgent extends Agent<Env, State> {
async onRequest(request: Request) {
this.queue("processItem", { id: "abc", data: "..." });
this.queue("processItem", { id: "def", data: "..." }, { retry: { maxAttempts: 5 } });
return new Response("Queued");
}
async processItem(payload: { id: string; data: string }, queueItem: QueueItem) {
await doWork(payload);
}
}
```
### Queue Management
```typescript
const items = this.getQueue();
const byCallback = this.getQueues("processItem");
this.dequeue(itemId);
this.dequeueAll();
this.dequeueAllByCallback("processItem");
```
## Retries
Exponential backoff with full jitter. Defaults: 3 attempts, 100ms base, 3000ms max.
```typescript
const result = await this.retry(
async () => {
const res = await fetch("https://api.example.com/data");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
{
maxAttempts: 5,
baseDelayMs: 200,
maxDelayMs: 5000,
shouldRetry: (err, nextAttempt) => {
if (err.message.includes("429")) return true;
if (err.message.includes("401")) return false;
return nextAttempt <= 3;
}
}
);
```
### Retry on Schedules and Queue
```typescript
await this.schedule(60, "task", payload, { retry: { maxAttempts: 3 } });
await this.scheduleEvery(30, "poll", undefined, { retry: { maxAttempts: 2 } });
this.queue("handler", payload, { retry: { maxAttempts: 5 } });
```
### Class-level Defaults
```typescript
export class MyAgent extends Agent<Env, State> {
static options = {
retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10000 }
};
}
```
## Important
- `shouldRetry` only works on `this.retry()` — not on schedule/queue (callbacks aren't serializable)
- Queue retries block head-of-line; long delays keep the DO awake — use `schedule` for long waits instead
- No dead-letter queue — failed items are removed after retries exhausted
@@ -0,0 +1,75 @@
# Routing
Fetch https://developers.cloudflare.com/agents/api-reference/routing/ for complete documentation.
## Default URL Pattern
`/agents/{kebab-class-name}/{instance-name}`
```typescript
import { routeAgentRequest } from "agents";
export default {
fetch: (req, env) =>
routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 })
};
```
| Class | URL |
|-------|-----|
| `Counter` | `/agents/counter/user-123` |
| `ChatRoom` | `/agents/chat-room/lobby` |
| `MyAgent` | `/agents/my-agent/default` |
Subpaths after the instance name (e.g. `/agents/my-agent/default/api/data`) route to `onRequest`.
## Custom Routing with `getAgentByName`
```typescript
import { getAgentByName } from "agents";
export default {
async fetch(req, env) {
const url = new URL(req.url);
if (url.pathname.startsWith("/api/")) {
const agent = getAgentByName(env.MyAgent, "singleton");
return agent.fetch(req);
}
return routeAgentRequest(req, env);
}
};
```
## Options
```typescript
routeAgentRequest(req, env, {
cors: true,
prefix: "/api/agents",
locationHint: "enam",
jurisdiction: "eu",
props: { userId: "123" },
onBeforeConnect: async (req) => { /* auth check */ },
onBeforeRequest: async (req) => { /* auth check */ }
});
```
`props` are delivered to `onStart(props)` on first access.
## Client Side
```tsx
useAgent({
agent: "MyAgent",
name: "instance-1",
host: "https://my-worker.workers.dev",
basePath: "/api/agents",
path: "/custom-subpath"
});
```
## Common Mistakes
- Class name `MyAgent` becomes kebab `my-agent` in URLs — match exactly
- "Namespace not found" error = the `class_name` in wrangler doesn't match your exported class
- If `sendIdentityOnConnect: false`, the `ready` promise on the client may never resolve — use state sync instead
@@ -0,0 +1,63 @@
# Server-Driven Messages (Trigger Patterns)
Fetch https://developers.cloudflare.com/agents/api-reference/trigger-patterns/ for complete documentation.
Patterns for server-initiated LLM turns in `AIChatAgent` — from schedules, webhooks, email, or other agents.
## `saveMessages` — Trigger an LLM Turn
```typescript
await this.saveMessages((existingMessages) => [
...existingMessages,
{ role: "user", content: "Check for new notifications" }
]);
```
`saveMessages` persists the messages AND triggers `onChatMessage`.
## `persistMessages` — Save Without Triggering
```typescript
await this.persistMessages([
...this.messages,
{ role: "assistant", content: "System note: checked at " + new Date() }
]);
```
## `waitUntilStable`
**Always call before `saveMessages` from non-chat contexts** (schedules, webhooks, email):
```typescript
async checkNotifications(payload: unknown, schedule: Schedule) {
await this.waitUntilStable({ timeout: 30_000 });
await this.saveMessages((msgs) => [
...msgs,
{ role: "user", content: "Run scheduled notification check" }
]);
}
```
## `onChatResponse`
Runs after each LLM turn completes. Use for chaining:
```typescript
async onChatResponse(result: ChatResponseResult) {
if (result.type === "finish" && needsFollowUp(result)) {
await this.saveMessages((msgs) => [
...msgs,
{ role: "user", content: "Continue with next step" }
]);
}
}
```
## Client Status
```tsx
const { isStreaming, isServerStreaming } = useAgentChat({ agent });
```
- `isStreaming` — true during any streaming (user-initiated or server-initiated)
- `isServerStreaming` — true only during server-initiated streams
@@ -0,0 +1,171 @@
# State & Scheduling
Fetch https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/ and https://developers.cloudflare.com/agents/api-reference/schedule-tasks/ for complete documentation.
## State Management
State persists to SQLite and broadcasts to connected clients automatically.
### Define Typed State
```typescript
type State = {
count: number;
items: string[];
};
export class MyAgent extends Agent<Env, State> {
initialState: State = { count: 0, items: [] };
}
```
### Read and Update
```typescript
// Read (lazy-loaded from SQLite)
const count = this.state.count;
// Write (sync, persists, broadcasts)
this.setState({ count: this.state.count + 1 });
```
### Validation Hook
`validateStateChange()` runs synchronously before state persists. Throw to reject the update.
```typescript
validateStateChange(nextState: State, source: Connection | "server") {
if (nextState.count < 0) {
throw new Error("Count cannot be negative");
}
}
```
### Execution Order
1. `validateStateChange(nextState, source)` - sync, gating
2. State persisted to SQLite
3. State broadcast to connected clients
4. `onStateUpdate(nextState, source)` - async via `ctx.waitUntil`, non-gating
### Client-Side Sync (React)
```tsx
import { useAgent } from "agents/react";
function App() {
const [state, setLocalState] = useState<State>({ count: 0 });
const agent = useAgent<State>({
agent: "MyAgent",
name: "instance-1",
onStateUpdate: (newState) => setLocalState(newState)
});
return <button onClick={() => agent.setState({ count: state.count + 1 })}>
Count: {state.count}
</button>;
}
```
## SQL API
Direct SQLite access for custom queries:
```typescript
// Create table
this.sql`
CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
name TEXT,
created_at INTEGER DEFAULT (unixepoch())
)
`;
// Insert
this.sql`INSERT INTO items (id, name) VALUES (${id}, ${name})`;
// Query with types
const items = this.sql<{ id: string; name: string }>`
SELECT * FROM items WHERE name LIKE ${`%${search}%`}
`;
```
## Scheduling
### Schedule Types
| Mode | Syntax | Use Case |
|------|--------|----------|
| Delay | `this.schedule(60, ...)` | Run in 60 seconds |
| Date | `this.schedule(new Date(...), ...)` | Run at specific time |
| Cron | `this.schedule("0 8 * * *", ...)` | Recurring schedule |
| Interval | `this.scheduleEvery(30, ...)` | Fixed interval (every 30s) |
### Examples
```typescript
// Delay (seconds)
await this.schedule(60, "checkStatus", { id: "abc123" });
// Specific date
await this.schedule(new Date("2025-12-25T00:00:00Z"), "sendGreeting", { to: "user" });
// Cron (recurring)
await this.schedule("0 9 * * 1-5", "weekdayReport", {});
// Fixed interval (every 30 seconds, overlap prevention built-in)
await this.scheduleEvery(30, "pollUpdates");
await this.scheduleEvery(300, "syncData", { source: "api" });
```
### Handler
```typescript
async sendGreeting(payload: { to: string }, schedule: Schedule) {
console.log(`Sending greeting to ${payload.to}`);
// Cron schedules auto-reschedule; one-time schedules are deleted
}
```
### Manage Schedules
```typescript
const schedules = this.getSchedules();
const crons = this.getSchedules({ type: "cron" });
await this.cancelSchedule(schedule.id);
```
### Retry on Schedules
```typescript
await this.schedule(60, "task", payload, { retry: { maxAttempts: 3 } });
await this.scheduleEvery(30, "poll", undefined, { retry: { maxAttempts: 2 } });
```
## Lifecycle Callbacks
```typescript
export class MyAgent extends Agent<Env, State> {
async onStart() {
// Agent started or woke from hibernation
}
onConnect(conn: Connection, ctx: ConnectionContext) {
// WebSocket connected
}
onMessage(conn: Connection, message: WSMessage) {
// WebSocket message (non-RPC)
}
onStateUpdate(state: State, source: Connection | "server") {
// State changed (async, non-blocking)
}
onError(error: unknown) {
// Error handler
throw error; // Re-throw to propagate
}
}
```
@@ -0,0 +1,198 @@
# Streaming Chat with AIChatAgent
Fetch https://developers.cloudflare.com/agents/api-reference/chat-agents/ for complete documentation.
`AIChatAgent` from `@cloudflare/ai-chat` provides streaming chat with automatic message persistence and resumable streams.
## Basic Chat Agent
```typescript
import { AIChatAgent } from "@cloudflare/ai-chat";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish, options) {
const result = streamText({
model: openai("gpt-4o"),
system: "You are a helpful assistant.",
messages: await convertToModelMessages(this.messages),
abortSignal: options?.abortSignal,
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
**Important:** Always pass `abortSignal` and `onFinish` — they enable proper cleanup and message persistence.
## With Tools
```typescript
import { tool } from "ai";
import { z } from "zod";
const tools = {
getWeather: tool({
description: "Get weather for a location",
parameters: z.object({ location: z.string() }),
execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`
})
};
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish, options) {
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
tools,
abortSignal: options?.abortSignal,
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## With Workers AI (no API keys)
```typescript
import { createWorkersAI } from "workers-ai-provider";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish, options) {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),
messages: await convertToModelMessages(this.messages),
abortSignal: options?.abortSignal,
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```
## Custom UI Message Stream
For more control, use `createUIMessageStream`:
```typescript
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
export class Chat extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(this.messages),
onFinish
});
writer.merge(result.toUIMessageStream());
}
});
return createUIMessageStreamResponse({ stream });
}
}
```
## Resumable Streaming
Streams automatically resume if client disconnects and reconnects:
1. Chunks buffered to SQLite during streaming
2. On reconnect, buffered chunks sent immediately
3. Live streaming continues from where it left off
**Enabled by default.** To disable:
```tsx
const { messages } = useAgentChat({ agent, resume: false });
```
## React Client
```tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function ChatUI() {
const agent = useAgent({
agent: "Chat",
name: "my-chat-session"
});
const {
messages,
input,
handleInputChange,
handleSubmit,
status
} = useAgentChat({ agent });
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
disabled={status === "streaming"}
/>
<button type="submit">Send</button>
</form>
</div>
);
}
```
## Streaming RPC Methods
For non-chat streaming, use `@callable({ streaming: true })`:
```typescript
import { Agent, callable, StreamingResponse } from "agents";
export class MyAgent extends Agent<Env> {
@callable({ streaming: true })
async streamData(stream: StreamingResponse, query: string) {
for (let i = 0; i < 10; i++) {
stream.send(`Result ${i}: ${query}`);
await sleep(100);
}
stream.close();
}
}
```
Client receives streamed messages via WebSocket RPC.
## Key Properties
| Property | Purpose |
|----------|---------|
| `this.messages` | All persisted messages |
| `maxPersistedMessages` | Limit stored messages (prune oldest) |
| `messageConcurrency` | `"queue"` (default), `"latest"`, `"merge"`, `"drop"` |
| `chatRecovery` | `"persist"` (default) or `"continue"` on reconnect |
| `waitForMcpConnections` | Wait for MCP servers before first turn |
## Status Values
`useAgentChat` status:
| Status | Meaning |
|--------|---------|
| `ready` | Idle, ready for input |
| `streaming` | Response streaming |
| `submitted` | Request sent, waiting |
| `error` | Error occurred |
Also: `isStreaming`, `isServerStreaming` for distinguishing user vs server-initiated streams.
@@ -0,0 +1,112 @@
# Think (Experimental)
Fetch https://developers.cloudflare.com/agents/api-reference/think/ for complete documentation.
`@cloudflare/think` — a higher-level chat agent class that handles the `streamText` loop, tool execution, and message persistence for you. You provide `getModel()` and `getSystemPrompt()`; Think handles the rest.
```bash
npm install @cloudflare/think
```
## Minimal Agent
```typescript
import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest } from "agents";
export class MyAgent extends Think<Env> {
getModel() {
return createWorkersAI({ binding: this.env.AI })("@cf/meta/llama-4-scout-17b-16e-instruct");
}
getSystemPrompt() {
return "You are a helpful assistant.";
}
}
export default {
fetch: (req, env) => routeAgentRequest(req, env)
};
```
## Wrangler Config
```jsonc
{
"compatibility_flags": ["nodejs_compat", "experimental"],
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }],
"ai": { "binding": "AI" }
}
```
**Note:** Think requires the `experimental` compatibility flag.
## Custom Tools
```typescript
import { tool } from "ai";
import { z } from "zod";
export class MyAgent extends Think<Env> {
getTools() {
return {
getWeather: tool({
description: "Get weather",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => `72°F in ${city}`
})
};
}
}
```
## Lifecycle Hooks
| Hook | When | Use for |
|------|------|---------|
| `configureSession()` | Agent starts | Set up memory, context providers |
| `beforeTurn(ctx)` | Before each LLM call | Per-turn model/tools/system prompt; return `TurnConfig` |
| `onChunk(chunk)` | Each streaming chunk | Progress tracking |
| `onChatResponse(result)` | After LLM turn completes | Chaining, follow-up `saveMessages` |
| `onChatError(error)` | On LLM error | Error handling |
```typescript
async beforeTurn(ctx: TurnContext): Promise<TurnConfig> {
if (ctx.continuation) {
return { model: cheaperModel };
}
return {};
}
```
## Sub-Agents
```typescript
const child = this.subAgent(SpecialistAgent, "specialist-1");
await child.chat("Analyze this data...", (chunk) => {
// stream callback
});
```
## Client
Same React hooks as `AIChatAgent`:
```tsx
const agent = useAgent({ agent: "MyAgent", name: "session-1" });
const { messages, input, handleInputChange, handleSubmit } = useAgentChat({ agent });
```
## Think vs AIChatAgent
| | Think | AIChatAgent |
|-|-------|-------------|
| `streamText` loop | Built-in | You write it |
| Tool execution | Automatic | You wire it |
| Customization | Override hooks | Full control in `onChatMessage` |
| Built-in tools | Workspace, execute, browser | None |
| Compatibility flag | Requires `experimental` | Standard |
@@ -0,0 +1,68 @@
# Voice (Experimental)
Fetch https://developers.cloudflare.com/agents/api-reference/voice/ for complete documentation.
`@cloudflare/voice` — real-time speech-to-text and text-to-speech for agents. Audio streams over WebSocket.
```bash
npm install @cloudflare/voice
```
## Server
```typescript
import { Agent } from "agents";
import { withVoice, WorkersAITTS, WorkersAINova3STT } from "@cloudflare/voice";
export class VoiceAgent extends withVoice(Agent)<Env> {
transcriber = new WorkersAINova3STT(this);
tts = new WorkersAITTS(this);
async onTurn(transcript: string, context: VoiceTurnContext) {
const result = streamText({
model: createWorkersAI({ binding: this.env.AI })("@cf/meta/llama-4-scout-17b-16e-instruct"),
messages: [
{ role: "system", content: "You are a voice assistant." },
...context.conversationHistory,
{ role: "user", content: transcript }
]
});
for await (const chunk of result.textStream) {
if (context.signal.aborted) break;
context.speak(chunk);
}
}
}
```
## Lifecycle Hooks
| Hook | Purpose |
|------|---------|
| `onTurn(transcript, ctx)` | Handle transcribed speech (required) |
| `beforeCallStart(conn)` | Auth/validation before call starts |
| `onCallStart(conn)` | Call connected |
| `onCallEnd(conn)` | Call disconnected |
| `onInterrupt()` | User interrupted agent speech |
## Client (React)
```tsx
import { useVoiceAgent } from "@cloudflare/voice/react";
function VoiceUI() {
const { isConnected, isSpeaking, connect, disconnect } = useVoiceAgent({
agent: "VoiceAgent",
name: "session-1"
});
return <button onClick={isConnected ? disconnect : connect}>
{isConnected ? "End Call" : "Start Call"}
</button>;
}
```
## STT/TTS Providers
Workers AI (default), Deepgram, ElevenLabs — install the provider package and swap the `transcriber`/`tts` properties.
@@ -0,0 +1,86 @@
# Webhooks & Push Notifications
## Webhooks
Fetch https://developers.cloudflare.com/agents/api-reference/webhooks/ for complete documentation.
Route external webhooks to agent instances via `onRequest`:
```typescript
export default {
async fetch(req: Request, env: Env) {
const url = new URL(req.url);
if (url.pathname.startsWith("/webhooks/")) {
const entityId = url.pathname.split("/")[2];
const agent = getAgentByName(env.MyAgent, entityId);
return agent.fetch(req);
}
return routeAgentRequest(req, env);
}
};
```
In the agent:
```typescript
export class MyAgent extends Agent<Env, State> {
async onRequest(request: Request) {
const signature = request.headers.get("X-Signature");
if (!verifySignature(signature, await request.text(), this.env.WEBHOOK_SECRET)) {
return new Response("Unauthorized", { status: 401 });
}
const payload = JSON.parse(await request.text());
this.queue("processWebhook", payload);
return new Response("OK", { status: 202 });
}
}
```
**Tips:** Respond quickly (200/202), verify signatures, deduplicate with stored event IDs, use `queue()` for async processing.
## Push Notifications
Fetch https://developers.cloudflare.com/agents/api-reference/push-notifications/ for complete documentation.
Web Push via VAPID from agents. Store subscriptions in agent state, send via `web-push`.
```bash
npm install web-push
```
```typescript
import webpush from "web-push";
export class NotifyAgent extends Agent<Env, State> {
@callable()
async subscribe(subscription: PushSubscription) {
this.setState({
...this.state,
subscriptions: [...this.state.subscriptions, subscription]
});
}
async sendReminder(payload: { message: string }, schedule: Schedule) {
for (const sub of this.state.subscriptions) {
try {
await webpush.sendNotification(sub, JSON.stringify({
title: "Reminder",
body: payload.message
}), {
vapidDetails: {
subject: "mailto:you@example.com",
publicKey: this.env.VAPID_PUBLIC_KEY,
privateKey: this.env.VAPID_PRIVATE_KEY
}
});
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 410) {
// Remove expired subscription
}
}
}
}
}
```
VAPID keys: generate with `npx web-push generate-vapid-keys`, store as secrets.
@@ -0,0 +1,132 @@
# Workflows Integration
Fetch https://developers.cloudflare.com/agents/api-reference/run-workflows/ for complete documentation.
## Overview
Agents handle real-time communication; Workflows handle durable execution. Together they enable:
- Long-running background tasks with automatic retries
- Human-in-the-loop approval flows
- Multi-step pipelines that survive failures
| Use Case | Recommendation |
|----------|----------------|
| Chat/messaging | Agent only |
| Quick API calls (<30s) | Agent only |
| Background processing (<30s) | Agent `queue()` |
| Long-running tasks (>30s) | Agent + Workflow |
| Human approval flows | Agent + Workflow |
## AgentWorkflow Base Class
```typescript
import { AgentWorkflow } from "agents/workflows";
import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";
type TaskParams = { taskId: string; data: string };
export class ProcessingWorkflow extends AgentWorkflow<MyAgent, TaskParams> {
async run(event: AgentWorkflowEvent<TaskParams>, step: AgentWorkflowStep) {
const params = event.payload;
// Durable step - retries on failure
const result = await step.do("process", async () => {
return processData(params.data);
});
// Non-durable: progress reporting
await this.reportProgress({ step: "process", percent: 0.5 });
// Non-durable: broadcast to connected clients
this.broadcastToClients({ type: "update", taskId: params.taskId });
// Durable: merge state via step
await step.mergeAgentState({ lastProcessed: params.taskId });
// Durable: report completion
await step.reportComplete(result);
return result;
}
}
```
## Wrangler Configuration
```jsonc
{
"workflows": [
{ "name": "processing-workflow", "binding": "PROCESSING_WORKFLOW", "class_name": "ProcessingWorkflow" }
],
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }]
}
```
## Agent Methods for Workflows
```typescript
// Start a workflow
const instance = await this.runWorkflow("ProcessingWorkflow", { taskId: "123", data: "..." });
// Send event to waiting workflow
await this.sendWorkflowEvent("ProcessingWorkflow", workflowId, { type: "approve" });
// Query workflows
const workflow = await this.getWorkflow(workflowId);
const workflows = await this.getWorkflows({ status: "running" });
// Control workflows
await this.approveWorkflow(workflowId);
await this.rejectWorkflow(workflowId);
await this.terminateWorkflow(workflowId);
await this.pauseWorkflow(workflowId);
await this.resumeWorkflow(workflowId);
// Delete workflows
await this.deleteWorkflow(workflowId);
await this.deleteWorkflows({ status: "complete", before: new Date(...) });
```
## Lifecycle Callbacks
```typescript
export class MyAgent extends Agent<Env, State> {
async onWorkflowProgress(workflowName: string, workflowId: string, progress: unknown) {
// Workflow reported progress via this.reportProgress()
this.broadcast({ type: "progress", workflowId, progress });
}
async onWorkflowComplete(workflowName: string, workflowId: string, result?: unknown) {
// Workflow finished successfully
}
async onWorkflowError(workflowName: string, workflowId: string, error: Error) {
// Workflow failed
}
async onWorkflowEvent(workflowName: string, workflowId: string, event: unknown) {
// Workflow received an event via sendWorkflowEvent()
}
}
```
## Human-in-the-Loop
```typescript
// In workflow: wait for approval
const approved = await step.waitForEvent<{ approved: boolean }>("approval", {
timeout: "7d"
});
if (!approved.approved) {
throw new Error("Rejected");
}
// From agent: approve or reject
await this.approveWorkflow(workflowId); // Sends { approved: true }
await this.rejectWorkflow(workflowId); // Sends { approved: false }
```
@@ -0,0 +1,103 @@
---
name: cloudflare-email-service
description: Send and receive transactional emails with Cloudflare Email Service (Email Sending + Email Routing). Use when building email sending (Workers binding or REST API), email routing, Agents SDK email handling, or integrating email into any app — Workers, Node.js, Python, Go, etc. Also use for email deliverability, SPF/DKIM/DMARC, wrangler email setup, MCP email tools, or when a coding agent needs to send emails. Even for simple requests like "add email to my Worker" — this skill has critical config details.
---
# Cloudflare Email Service
Your knowledge of the Cloudflare Email Service, Email Routing or Email Sending may be outdated. **Prefer retrieval over pre-training** for any Cloudflare Email Service task.
Cloudflare Email Service lets you send transactional emails and route incoming emails, all within the Cloudflare platform. Your knowledge of this product may be outdated — it launched in 2025 and is evolving rapidly. **Prefer retrieval over pre-training** for any Email Service task.
**If there is any discrepancy between this skill and the sources below, always trust the original source.** The Cloudflare docs, REST API spec, `@cloudflare/workers-types`, and Agents SDK repo are the source of truth. This skill is a convenience guide — it may lag behind the latest changes. When in doubt, retrieve from the sources below and use what they say.
## Retrieval Sources
| Source | How to retrieve | Use for |
|--------|----------------|---------|
| Cloudflare docs | `cloudflare-docs` search tool or URL `https://developers.cloudflare.com/email-service/` | API reference, limits, pricing, latest features |
| REST API spec | `https://developers.cloudflare.com/api/resources/email_sending` | OpenAPI spec for the Email Sending REST API |
| Workers types | `https://www.npmjs.com/package/@cloudflare/workers-types` | Type signatures, binding shapes |
| Agents SDK docs | Fetch `docs/email.md` from `https://github.com/cloudflare/agents/tree/main/docs` | Email handling in Agents SDK |
## FIRST: Check Prerequisites
Before writing any email code, verify the basics are in place:
1. **Domain onboarded?** Run `npx wrangler email sending list` to see which domains have email sending enabled. If the domain isn't listed, run `npx wrangler email sending enable userdomain.com` or see [cli-and-mcp.md](references/cli-and-mcp.md) for full setup instructions.
2. **Binding configured?** Look for `send_email` in `wrangler.jsonc` (for Workers)
3. **postal-mime installed?** Run `npm ls postal-mime` (only needed for receiving/parsing emails)
## What Do You Need?
Start here. Find your situation, then follow the link for full details.
| I want to... | Path | Reference |
|--------------|------|-----------|
| **Send emails from a Cloudflare Worker** | Workers binding (no API keys needed) | [sending.md](references/sending.md) |
| **Send emails from an AI agent built with [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/)** | `onEmail()` + `replyToEmail()` in Agent class | [sending.md](references/sending.md) |
| **Send emails from an external app or agent** (Node.js, Go, Python, etc.) | REST API with Bearer token | [rest-api.md](references/rest-api.md) |
| **Send emails from a coding agent** (Claude Code, Cursor, Copilot, etc.) | MCP tools, wrangler CLI, or REST API | [cli-and-mcp.md](references/cli-and-mcp.md) |
| **Receive and process incoming emails** (Email Routing) | Workers `email()` handler | [routing.md](references/routing.md) |
| **Set up Email Sending or Email Routing** | `wrangler email sending enable` / `wrangler email routing enable`, or Dashboard | [cli-and-mcp.md](references/cli-and-mcp.md) |
| **Improve deliverability, avoid spam folders** | Authentication, content, compliance | [deliverability.md](references/deliverability.md) |
## Quick Start — Workers Binding
Add the binding to `wrangler.jsonc`, then call `env.EMAIL.send()`. The `from` domain must be onboarded via `npx wrangler email sending enable yourdomain.com`.
```jsonc
// wrangler.jsonc
{ "send_email": [{ "name": "EMAIL" }] }
```
```typescript
const response = await env.EMAIL.send({
to: "user@example.com",
from: { email: "welcome@yourdomain.com", name: "My App" },
subject: "Welcome!",
html: "<h1>Welcome!</h1>",
text: "Welcome!",
});
```
The binding is recommended for Workers — no API keys needed. If a user specifically requests the REST API from within a Worker (e.g., they already have an API token workflow), that works too — see [rest-api.md](references/rest-api.md).
See [sending.md](references/sending.md) for the full API, batch sends, attachments, custom headers, restricted bindings, and Agents SDK integration.
## Quick Start — REST API
For apps outside Workers, or within Workers if the user explicitly requests it. Key differences from the Workers binding:
- Endpoint: `POST https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send`
- `from` object uses `address` (not `email`): `{ "address": "...", "name": "..." }`
- `replyTo` is `reply_to` (snake_case)
- Response returns `{ delivered: [], permanent_bounces: [], queued: [] }` (not `messageId`)
See [rest-api.md](references/rest-api.md) for curl examples, response format, and error handling.
## Common Mistakes
| Mistake | Why It Happens | Fix |
|---------|---------------|-----|
| Forgetting `send_email` binding in wrangler config | Email Service uses a binding, not an API key | Add `"send_email": [{ "name": "EMAIL" }]` to wrangler.jsonc |
| Sending from an unverified domain | Domain must be onboarded onto Email Sending before first send | Run `wrangler email sending enable yourdomain.com` or onboard in Dashboard |
| Reading `message.raw` twice in email handler | The raw stream is single-use — second read returns empty | Buffer first: `const raw = await new Response(message.raw).arrayBuffer()` |
| Missing `text` field (HTML only) | Some email clients only show plain text; also helps spam scores | Always include both `html` and `text` versions |
| Using email for marketing/bulk sends | Email Service is for transactional email only | Use a dedicated marketing email platform for newsletters and campaigns |
| Forwarding to unverified destinations | `message.forward()` only works with verified addresses | Run `wrangler email routing addresses create user@gmail.com` or add in Dashboard |
| Testing with fake addresses | Bounces from non-existent addresses hurt sender reputation | Use real addresses you control during development |
| Hardcoding API tokens in source code | Tokens in code get committed and leaked | Use environment variables or Cloudflare secrets |
| Ignoring the `from` domain requirement | The `from` address must use a domain onboarded to Email Service | Verify the domain first, then send from `anything@that-domain.com` |
| Using `email` key in REST API `from` object | REST API uses `address` not `email` for `from` object | Use `{ "address": "...", "name": "..." }` for REST, `{ "email": "...", "name": "..." }` for Workers |
| Using `replyTo` in REST API | REST API uses snake_case field names | Use `reply_to` for REST API, `replyTo` for Workers binding |
## References
Read the reference that matches your situation. You don't need all of them.
- **[references/sending.md](references/sending.md)** — Workers binding API, attachments, Agents SDK email. For Workers or Agents SDK.
- **[references/rest-api.md](references/rest-api.md)** — REST endpoint, curl examples, error handling. For apps NOT on Workers.
- **[references/routing.md](references/routing.md)** — Inbound `email()` handler, forwarding, replying, parsing. For receiving emails.
- **[references/cli-and-mcp.md](references/cli-and-mcp.md)** — Domain setup, wrangler commands, MCP tools. For first-time setup.
- **[references/deliverability.md](references/deliverability.md)** — SPF/DKIM/DMARC, bounces, suppressions, best practices.
@@ -0,0 +1,125 @@
# CLI, MCP, and Project Setup
Manage Cloudflare Email Service from the command line and coding agents.
For full CLI reference, run `npx wrangler email --help`. For Dashboard setup, see the [getting started docs](https://developers.cloudflare.com/email-service/get-started/).
## Wrangler Email Commands
```
wrangler email routing
├── enable/disable <domain> # Toggle email routing
├── dns get <domain> # Show required DNS records
├── rules list/create/update/delete # Manage routing rules
└── addresses list/create/delete # Destination addresses (account-scoped)
wrangler email sending
├── enable/disable <domain> # Toggle email sending
├── dns get <domain> # Show sending DNS records (SPF, DKIM)
├── send --from --to ... # Send an email (builder flags)
└── send-raw --from --to ... # Send a raw MIME email
```
## Domain Setup
### Via Dashboard
1. Navigate to **Compute & AI** > **Email Service** > **Email Sending** (or **Email Routing**)
2. Select **Onboard Domain** > choose domain > **Add records and onboard**
This auto-adds SPF (TXT) and DKIM (CNAME/TXT) records. DNS usually propagates within 5-15 minutes.
### Via CLI
```bash
npx wrangler email sending enable yourdomain.com
npx wrangler email sending dns get yourdomain.com # Verify records
```
## Local Development
Add `"remote": true` to send real emails during `wrangler dev`:
```jsonc
{ "send_email": [{ "name": "EMAIL", "remote": true }] }
```
```bash
npx wrangler dev
```
Emails are actually sent — use test addresses you control. Remove `"remote": true` before deploying.
## Cloudflare MCP Server
If you have the [Cloudflare MCP server](https://github.com/cloudflare/mcp) (`https://mcp.cloudflare.com/mcp`) configured, you can manage Email Service through its `search` and `execute` tools.
Use `search` to find email sending endpoints:
```javascript
// search tool — find all email sending API endpoints
async () => {
const results = [];
for (const [path, methods] of Object.entries(spec.paths)) {
if (path.includes('email/sending')) {
for (const [method, op] of Object.entries(methods)) {
results.push({ method: method.toUpperCase(), path, summary: op.summary });
}
}
}
return results;
}
```
Then use `execute` to call them — for example, checking sending limits or sending an email:
```javascript
// execute tool — check sending quota
async () => {
return cloudflare.request({
method: "GET",
path: `/accounts/${accountId}/email/sending/limits`
});
}
// execute tool — send an email
async () => {
return cloudflare.request({
method: "POST",
path: `/accounts/${accountId}/email/sending/send`,
body: {
to: "user@example.com",
from: { address: "notifications@yourdomain.com", name: "My App" },
subject: "Deployment Complete",
html: "<h1>Deployed!</h1>",
text: "Deployed!"
}
});
}
```
GraphQL analytics queries also work through `execute` — see [deliverability.md](deliverability.md#graphql-analytics-api) for query examples. Note that email analytics are **zone-level** datasets (`emailSendingAdaptiveGroups`, `emailSendingAdaptive`) queried under `viewer > zones`, and require the **Analytics Read** token permission.
## Sending from CLI / Agents
```bash
npx wrangler email sending send \
--from "agent@yourdomain.com" \
--to "developer@company.com" \
--subject "Deployment Complete" \
--text "Your Worker was deployed successfully."
```
Or via REST API:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/email/sending/send" \
--header "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
--header "Content-Type: application/json" \
--data '{
"to": "developer@company.com",
"from": {"address": "agent@yourdomain.com", "name": "Build Agent"},
"subject": "Deployment Complete",
"text": "Your Worker was deployed successfully."
}'
```
@@ -0,0 +1,285 @@
# Email Deliverability & Best Practices
For full details, see the [deliverability docs](https://developers.cloudflare.com/email-service/concepts/deliverability/) and [email authentication docs](https://developers.cloudflare.com/email-service/concepts/email-authentication/). All the monitoring endpoints below can be called via the [REST API](rest-api.md), [Wrangler CLI, or the Cloudflare MCP server](cli-and-mcp.md).
## What Cloudflare Handles
When you onboard a domain, Cloudflare auto-configures:
- **SPF** — TXT records authorizing Cloudflare's sending infrastructure
- **DKIM** — Records for cryptographic signing of outbound emails
- **IP reputation** — Managed sending infrastructure optimized for deliverability
- **Soft bounce retries** — Automatic exponential backoff for temporary failures
- **Suppression lists** — Hard-bounced addresses automatically blocked
- **Feedback loops** — ISP complaint signals processed and acted on
Consider adding a **DMARC** record if you don't have one: `v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@yourdomain.com`
## Bounce Handling
**Hard bounces** — permanent failures (address doesn't exist, domain doesn't exist). Never retried. Address auto-added to suppression list. Sending to suppressed address returns `E_RECIPIENT_SUPPRESSED`.
**Soft bounces** — temporary failures (mailbox full, server down, greylisting). Cloudflare auto-retries with exponential backoff.
## Suppression Lists
**Account list** (your account) — spam complaints from recipients. Cloudflare integrates with Postmasters to auto-suppress. You can manually add/remove addresses in the Dashboard.
See the [suppressions docs](https://developers.cloudflare.com/email-service/concepts/suppressions/) for details.
## Your Responsibilities
### Content
- Include both HTML and plain text versions
- Use a recognizable sender name: `{ email: "noreply@app.com", name: "My App" }`
- Write honest subject lines — avoid ALL CAPS, excessive punctuation
- Include `List-Unsubscribe` headers for recurring emails
- Use full URLs from your domain — avoid URL shorteners
### List Quality
- Validate email addresses before sending
- Implement double opt-in for subscriptions
- Honor unsubscribe requests promptly
### Transactional Only
Email Service is for **transactional email** (triggered by user actions: signups, password resets, order confirmations). Marketing/bulk campaigns are not permitted — use a dedicated marketing platform.
## Monitoring Deliverability
### Dashboard
Per-domain and account-wide analytics are available in the Cloudflare dashboard:
1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com) and select your account.
2. Go to **Compute & AI** > **Email Service**.
3. Select a domain or view account-wide metrics.
4. Select the **Analytics** tab.
### Send Response
Every send (REST API or Workers binding) returns immediate delivery feedback. Check the response to track per-send outcomes:
```json
{
"result": {
"delivered": ["user@example.com"],
"permanent_bounces": ["bad@nonexistent.com"],
"queued": ["slow@recipient.com"]
}
}
```
Log these to build your own delivery rate metrics.
### Sending Limits
Check your account's daily sending quota:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/limits" \
--header "Authorization: Bearer <API_TOKEN>"
```
Returns:
```json
{
"result": {
"quota": { "value": 5000, "unit": "day" }
}
}
```
### Suppression List
Addresses that hard-bounced or received spam complaints are auto-suppressed. You can query and manage suppressions via the API.
**List suppressions (account-wide):**
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression?page=1&per_page=100&order=created_at&direction=desc" \
--header "Authorization: Bearer <API_TOKEN>"
```
Returns:
```json
{
"page": 1,
"per_page": 100,
"total": 2,
"result": [
{
"id": "396a5436-d4b0-42a6-b3fc-48e8fa522321",
"email": "bounced@example.com",
"reason": "hard_bounce",
"created_at": "2026-03-15T10:00:00Z",
"expires_at": null
}
]
}
```
Query params: `page`, `per_page` (max 1000), `order` (`email` | `expires_at` | `created_at`), `direction` (`asc` | `desc`).
**Manually suppress an address:**
```bash
curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{ "email": "user@example.com", "expires_at": "2026-06-01T00:00:00Z" }'
```
`expires_at` is optional — omit for permanent suppression.
**Remove a suppression:**
```bash
curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression/{suppression_id}" \
--header "Authorization: Bearer <API_TOKEN>"
```
Zone-level suppressions are also available at `/zones/{zone_id}/email/sending/suppression` with the same interface.
### GraphQL Analytics API
Email Service exposes two zone-level datasets via the [GraphQL Analytics API](https://developers.cloudflare.com/analytics/graphql-api/). You can explore the schema interactively at [graphql.cloudflare.com/explorer](https://graphql.cloudflare.com/explorer). Metrics are retained for 31 days.
| Dataset | Description |
|---------|-------------|
| `emailSendingAdaptiveGroups` | Aggregated counts grouped by dimensions (status, date, domain, auth results, etc.) |
| `emailSendingAdaptive` | Individual email events with full detail (from, to, subject, messageId, errors, etc.) |
These are **zone-level** datasets — query under `viewer > zones`, not `accounts`.
**Aggregated dimensions** (`emailSendingAdaptiveGroups`):
| Dimension | Type | Description |
|-----------|------|-------------|
| `date` | Date | Day-level grouping |
| `datetime` | Time | Exact timestamp (also: `datetimeMinute`, `datetimeFiveMinutes`, `datetimeFifteenMinutes`, `datetimeHour`) |
| `status` | string | Delivery status |
| `eventType` | string | Event type |
| `sendingDomain` | string | The sending domain |
| `envelopeTo` | string | Recipient address |
| `errorCause` | string | Error cause for failed sends |
| `arc`, `dkim`, `dmarc`, `spf` | string | Email authentication results |
| `isSpam`, `isNDR`, `isLastEvent` | uint8 | Boolean flags |
| `spamScore`, `spamThreshold` | uint32 | Spam scoring |
**Individual event fields** (`emailSendingAdaptive`) additionally include: `from`, `to`, `subject`, `messageId`, `sessionId`, `errorDetail`.
**Email counts by status and date:**
```graphql
query EmailSendingByStatus(
$zoneTag: string!
$start: Date!
$end: Date!
) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
emailSendingAdaptiveGroups(
filter: { date_geq: $start, date_leq: $end }
limit: 10000
orderBy: [date_DESC]
) {
count
dimensions {
date
status
}
}
}
}
}
```
**Filter by status (e.g. only failures):**
```graphql
query EmailFailures(
$zoneTag: string!
$start: Date!
$end: Date!
) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
emailSendingAdaptiveGroups(
filter: { date_geq: $start, date_leq: $end, status: "deliveryFailed" }
limit: 10000
orderBy: [date_DESC]
) {
count
dimensions {
date
errorCause
sendingDomain
}
}
}
}
}
```
**Individual email events (troubleshooting):**
```graphql
query RecentEmailEvents(
$zoneTag: string!
$start: Time!
$end: Time!
) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
emailSendingAdaptive(
filter: { datetime_geq: $start, datetime_leq: $end }
limit: 50
orderBy: [datetime_DESC]
) {
datetime
from
to
subject
status
eventType
sendingDomain
messageId
errorCause
errorDetail
dkim
dmarc
spf
isSpam
}
}
}
}
```
Note: `emailSendingAdaptive` filters use `datetime_geq`/`datetime_leq` (Time type, e.g. `"2026-04-01T00:00:00Z"`), while `emailSendingAdaptiveGroups` uses `date_geq`/`date_leq` (Date type, e.g. `"2026-04-01"`).
**curl example:**
```bash
curl "https://api.cloudflare.com/client/v4/graphql" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"query": "query($zoneTag:string!,$start:Date!,$end:Date!){viewer{zones(filter:{zoneTag:$zoneTag}){emailSendingAdaptiveGroups(filter:{date_geq:$start,date_leq:$end},limit:10000,orderBy:[date_DESC]){count,dimensions{date,status}}}}}",
"variables": {
"zoneTag": "<ZONE_ID>",
"start": "2026-03-15",
"end": "2026-04-15"
}
}'
```
## Metrics to Watch
| Metric | Target | If Out of Range |
|--------|--------|-----------------|
| Delivery rate | > 95% | Check for invalid addresses; verify DNS records |
| Hard bounce rate | < 2% | Clean your email list |
| Complaint rate | < 0.1% | Make unsubscribe easier; stop unwanted emails |
@@ -0,0 +1,184 @@
# Sending Emails — REST API
Send emails via HTTP requests from any application. If your app runs on Cloudflare Workers, use the [Workers binding](sending.md) instead — simpler and more performant.
For the full OpenAPI specification, refer to the [Email Sending API reference](https://developers.cloudflare.com/api/resources/email_sending/methods/send).
## Endpoint
```
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send
```
## Authentication
Authenticate with a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) that has email sending permission:
```
Authorization: Bearer <API_TOKEN>
```
## Request Fields
**IMPORTANT:** The REST API uses different field names than the Workers binding in some cases.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | string or string[] | Yes | Recipient(s), max 50 combined with cc/bcc |
| `from` | string or `{address, name}` | Yes | Sender — **uses `address` not `email`** for object form |
| `subject` | string | Yes | Email subject line |
| `html` | string | No* | HTML body |
| `text` | string | No* | Plain text body |
| `cc` | string or string[] | No | CC recipients |
| `bcc` | string or string[] | No | BCC recipients |
| `reply_to` | string or `{address, name}` | No | **Snake_case**, not camelCase |
| `attachments` | array | No | File attachments and inline images |
| `headers` | object | No | Custom email headers |
*At least one of `html` or `text` required. Include both for best deliverability.
**Key differences from Workers binding:** `from` uses `address` (REST) vs `email` (Workers), `reply_to` (REST) vs `replyTo` (Workers), `content_id` (REST) vs `contentId` (Workers) for inline attachments, errors use numeric codes (REST) vs string `E_*` codes (Workers), and responses return delivery status (`delivered`/`permanent_bounces`/`queued`) instead of just `messageId`.
## Examples
Simple send:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": "user@example.com",
"from": "welcome@yourdomain.com",
"subject": "Welcome!",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
"text": "Welcome! Thanks for signing up."
}'
```
Multiple recipients with CC/BCC:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": ["user1@example.com", "user2@example.com"],
"cc": ["manager@company.com"],
"bcc": ["archive@company.com"],
"from": { "address": "orders@yourdomain.com", "name": "Orders" },
"reply_to": "support@yourdomain.com",
"subject": "Order Confirmation",
"html": "<h1>Your order is confirmed</h1>",
"text": "Your order is confirmed"
}'
```
With attachments:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": "customer@example.com",
"from": "invoices@yourdomain.com",
"subject": "Your Invoice",
"html": "<h1>Invoice attached</h1>",
"attachments": [
{
"content": "JVBERi0xLjQKJeLjz9MK...",
"filename": "invoice.pdf",
"type": "application/pdf",
"disposition": "attachment"
}
]
}'
```
With inline images (embedded via `cid:` URI):
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": "user@example.com",
"from": "newsletter@yourdomain.com",
"subject": "Check this out",
"html": "<h1>Hello!</h1><img src=\"cid:logo\">",
"attachments": [
{
"content": "iVBORw0KGgoAAAANSUhEUg...",
"filename": "logo.png",
"type": "image/png",
"disposition": "inline",
"content_id": "logo"
}
]
}'
```
Attachment fields differ by type:
- **Regular attachment**: `content` (base64), `filename`, `type` (MIME), `disposition: "attachment"`
- **Inline image**: `content` (base64), `filename`, `type` (MIME), `disposition: "inline"`, `content_id` (referenced in HTML as `cid:<content_id>`)
With custom headers:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": "user@example.com",
"from": "notifications@yourdomain.com",
"subject": "Your weekly digest",
"html": "<h1>Weekly Digest</h1>",
"headers": {
"List-Unsubscribe": "<https://yourdomain.com/unsubscribe?id=abc123>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
"X-Campaign-ID": "weekly-digest-2026-03"
}
}'
```
See the [email headers reference](https://developers.cloudflare.com/email-service/reference/headers/) for the full list of allowed headers.
## Response
Success:
```json
{
"success": true,
"errors": [],
"messages": [],
"result": {
"delivered": ["recipient@example.com"],
"permanent_bounces": [],
"queued": []
}
}
```
Error (numeric codes, not `E_*` string codes like Workers):
```json
{
"success": false,
"errors": [{ "code": 1000, "message": "Sender domain not verified" }],
"result": null
}
```
## Error Handling
| Status | Meaning | Retry? |
|--------|---------|--------|
| 200 | Success | N/A |
| 400 | Validation error | No — fix the request |
| 401 | Invalid API token | No — check your token |
| 429 | Rate limited | Yes — exponential backoff |
| 500 | Server error | Yes — exponential backoff |
Only retry on 429 and 500. Validation errors (400) won't succeed on retry.
@@ -0,0 +1,199 @@
# Receiving & Routing Inbound Email
Handle incoming emails sent to your domain via a Worker's `email()` handler. Forward, reply, reject, or parse emails programmatically.
For full API details, see the [Email Routing docs](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/).
## Email Handler
Export an `email()` function from your Worker. No special wrangler binding needed — a routing rule connects incoming addresses to your Worker.
```typescript
export default {
async email(message, env, ctx): Promise<void> {
console.log(`Email from ${message.from} to ${message.to}`);
await message.forward("team@company.com");
},
} satisfies ExportedHandler<Env>;
```
Set up routing rules in **Dashboard** > **Compute & AI** > **Email Service** > **Email Routing** > **Routing Rules**, or via `wrangler email routing rules create`.
## ForwardableEmailMessage
The `message` parameter is a `ForwardableEmailMessage`. Run `npx wrangler types` to get the full type definition. Key properties and methods:
- `message.from` / `message.to` — envelope addresses (SMTP MAIL FROM / RCPT TO). `message.from` is trustworthy; header addresses can be spoofed.
- `message.headers` — `Headers` object (use `.get("subject")`, `.get("message-id")`, etc.)
- `message.raw` — `ReadableStream<Uint8Array>` of raw MIME content. **Single use** — buffer before accessing.
- `message.rawSize` — size in bytes
- `message.setReject(reason)` — reject with a permanent SMTP error
- `message.forward(rcptTo, headers?)` — forward to a verified destination
- `message.reply(emailMessage)` — reply with an `EmailMessage` object
## Core Operations
### Forward
```typescript
await message.forward("team@company.com");
// With custom headers
await message.forward("team@company.com", new Headers({
"X-Original-Recipient": message.to,
}));
```
Destination must be verified first (Dashboard or `wrangler email routing addresses create`).
### Reject
```typescript
message.setReject("Your message was blocked");
```
### Reply
Using `env.EMAIL.send()` (recommended — no extra dependencies):
```typescript
async email(message, env, ctx) {
const subject = message.headers.get("subject") || "";
await env.EMAIL.send({
to: message.from,
from: message.to,
subject: `Re: ${subject}`,
html: "<p>Thanks! We'll respond shortly.</p>",
text: "Thanks! We'll respond shortly.",
});
await message.forward("team@company.com");
}
```
Using `message.reply()` with MIME (more control, requires `mimetext` + `nodejs_compat`):
```typescript
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
async email(message, env, ctx) {
const msg = createMimeMessage();
const messageId = message.headers.get("Message-ID");
if (messageId) msg.setHeader("In-Reply-To", messageId);
msg.setSender({ name: "Support", addr: "support@yourdomain.com" });
msg.setRecipient(message.from);
msg.setSubject("Re: " + (message.headers.get("subject") || ""));
msg.addMessage({ contentType: "text/plain", data: "Thanks for reaching out!" });
await message.reply(new EmailMessage("support@yourdomain.com", message.from, msg.asRaw()));
}
```
## Parsing Emails
Use [postal-mime](https://www.npmjs.com/package/postal-mime) to parse raw MIME content:
```typescript
import PostalMime from "postal-mime";
async email(message, env, ctx) {
const rawBuffer = await new Response(message.raw).arrayBuffer();
const parsed = await PostalMime.parse(rawBuffer);
console.log("Subject:", parsed.subject);
console.log("Text:", parsed.text);
console.log("Attachments:", parsed.attachments.length);
}
```
## Store and Reply Later (Human-in-the-Loop)
A common pattern is to store incoming emails in a Durable Object (SQLite) so a human or AI agent can review and reply later — rather than replying immediately in the `email()` handler. This enables support inboxes, approval workflows, and AI-drafted replies.
### Architecture
```
Email Routing → email() handler → parse + store in DO → user reviews later → reply via send_email binding
```
The `email()` handler stores the email and returns immediately. Replies happen later via a separate HTTP request or agent action.
### Receive and Store
```typescript
import PostalMime from "postal-mime";
export class MailboxDO extends DurableObject {
async storeEmail(from: string, to: string, subject: string, body: string,
messageId: string, inReplyTo: string | null) {
this.ctx.storage.sql.exec(
`INSERT INTO emails (sender, recipient, subject, body, message_id, in_reply_to, date, read)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), 0)`,
from, to, subject, body, messageId, inReplyTo
);
}
}
export default {
async email(message, env, ctx) {
const raw = await new Response(message.raw).arrayBuffer();
const parsed = await PostalMime.parse(raw);
const id = env.MAILBOX.idFromName(message.to);
const stub = env.MAILBOX.get(id);
await stub.storeEmail(
message.from,
message.to,
parsed.subject || "(no subject)",
parsed.text || parsed.html || "",
message.headers.get("message-id") || "",
message.headers.get("in-reply-to") || null,
);
// Optionally trigger an AI agent to draft a reply (non-blocking)
// ctx.waitUntil(notifyAgent(env, message.to, emailId));
},
} satisfies ExportedHandler<Env>;
```
### Reply Later
When a user (or agent) decides to reply, build proper threading headers and send via the `send_email` binding:
```typescript
// In an HTTP handler or agent tool — not in the email() handler
async function replyToStoredEmail(env: Env, original: StoredEmail, replyBody: string) {
// Build threading headers (In-Reply-To + References per RFC 2822)
const headers: Record<string, string> = {};
if (original.messageId) {
headers["In-Reply-To"] = original.messageId;
headers["References"] = original.messageId;
}
await env.EMAIL.send({
to: original.sender,
from: original.recipient,
subject: `Re: ${original.subject}`,
text: replyBody,
html: `<p>${replyBody}</p>`,
headers,
});
}
```
### Key Design Points
- **Buffer `message.raw` once**, parse with `postal-mime`, then store structured fields. Don't store the raw stream.
- **Extract `Message-ID`, `In-Reply-To`, and `References`** headers during ingest for threading. Fall back to subject-based matching for emails without threading headers.
- **Use Durable Object SQLite** for per-mailbox storage — each mailbox gets its own DO instance keyed by email address, providing natural isolation.
- **Store attachments separately** in R2 (binary blobs), with metadata in SQLite.
- **Defer heavy work** (AI drafting, notifications) via `ctx.waitUntil()` so the `email()` handler returns quickly.
- **Never auto-send from the `email()` handler** in a human-in-the-loop flow. Store a draft, let the user review, then send via a separate action.
## Gotchas
- **`message.raw` is single-use.** Buffer first: `const raw = await new Response(message.raw).arrayBuffer()`
- **Destinations must be verified.** Forwarding to unverified addresses fails silently.
- **Handler must act.** If your handler returns without consuming raw, forwarding, or rejecting, the email is dropped.
- **DMARC/SPF for replies.** If sending replies, ensure your domain has proper SPF/DKIM records (auto-configured on domain onboarding).
@@ -0,0 +1,252 @@
# Sending Emails — Workers Binding & Agents SDK
Send emails from Cloudflare Workers using the native binding, or from AI agents using the Agents SDK. If your app is NOT on Workers, use the [REST API](rest-api.md) instead.
## Workers Binding
### Configuration
```jsonc
// wrangler.jsonc
{
"send_email": [
{ "name": "EMAIL" }
]
}
```
For local development, add `"remote": true` so email sends are proxied to the real service:
```jsonc
{ "send_email": [{ "name": "EMAIL", "remote": true }] }
```
Run `npx wrangler types` to auto-generate the `Env` interface with your `EMAIL` binding. This produces a `worker-configuration.d.ts` with the real `SendEmail`, `EmailAttachment`, `EmailAddress`, and related types from the workerd runtime. Always use these generated types — do not define them manually.
**Note:** Workers binding uses `email` in the from object (`EmailAddress` type). REST API uses `address`. See [rest-api.md](rest-api.md).
## send()
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await env.EMAIL.send({
to: "user@example.com",
from: { email: "welcome@yourdomain.com", name: "My App" },
subject: "Welcome!",
html: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
text: "Welcome! Thanks for signing up.",
});
return new Response(`Sent: ${response.messageId}`);
},
} satisfies ExportedHandler<Env>;
```
Multiple recipients (max 50 combined to + cc + bcc):
```typescript
const response = await env.EMAIL.send({
to: ["user1@example.com", "user2@example.com"],
cc: ["manager@company.com"],
bcc: ["archive@company.com"],
from: { email: "orders@yourdomain.com", name: "Orders" },
replyTo: "support@yourdomain.com",
subject: "Order Confirmation #12345",
html: "<h1>Your order is confirmed</h1>",
text: "Your order is confirmed",
});
```
## Attachments
**Important:** The Workers binding and REST API handle attachment content differently:
- **Workers binding:** `content` accepts `string | ArrayBuffer | ArrayBufferView`. Strings are treated as **raw content** (not base64). For text files, pass the raw string. For binary files (images, PDFs), pass an `ArrayBuffer`. Note: `ArrayBuffer` does not work with `"remote": true` in local dev — deploy to test binary attachments.
- **REST API:** `content` is always a **base64-encoded string**. See [rest-api.md](rest-api.md).
```typescript
// Text file attachment — pass raw string content
const response = await env.EMAIL.send({
to: "customer@example.com",
from: "invoices@yourdomain.com",
subject: "Your Report",
html: "<h1>Report attached</h1>",
text: "Report attached.",
attachments: [{
content: "Name,Amount\nWidget A,100\nWidget B,250", // Raw text, NOT base64
filename: "report.csv",
type: "text/csv",
disposition: "attachment",
}],
});
// Binary file attachment — use ArrayBuffer
const pdfBytes = await fetchPdfFromSomewhere(); // Returns ArrayBuffer
const response = await env.EMAIL.send({
to: "customer@example.com",
from: "invoices@yourdomain.com",
subject: "Your Invoice",
html: "<h1>Invoice attached</h1>",
text: "Invoice attached.",
attachments: [{
content: pdfBytes,
filename: "invoice-12345.pdf",
type: "application/pdf",
disposition: "attachment",
}],
});
// Inline image — reference in HTML with cid:<contentId>
const imageBytes = await fetchImageFromSomewhere(); // Returns ArrayBuffer
const response = await env.EMAIL.send({
to: "user@example.com",
from: "marketing@yourdomain.com",
subject: "New Product",
html: '<img src="cid:product-hero" alt="Product" />',
attachments: [{
content: imageBytes,
filename: "product.png",
type: "image/png",
disposition: "inline",
contentId: "product-hero",
}],
});
```
Total email size (body + attachments) cannot exceed 25 MiB.
## Custom Headers
Only whitelisted headers allowed. See the [headers reference](https://developers.cloudflare.com/email-service/reference/headers/).
```typescript
const response = await env.EMAIL.send({
to: "user@example.com",
from: "notifications@yourdomain.com",
subject: "Your weekly digest",
html: "<h1>Weekly Digest</h1>",
headers: {
"In-Reply-To": "<original-message-id@yourdomain.com>",
"List-Unsubscribe": "<https://yourdomain.com/unsubscribe?id=abc123>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
"X-Campaign-ID": "weekly-digest-2026-03",
},
});
```
## Legacy EmailMessage API
The `EmailMessage` API remains supported. Uses raw MIME via `mimetext`:
```typescript
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
const msg = createMimeMessage();
msg.setSender({ name: "Sender", addr: "sender@yourdomain.com" });
msg.setRecipient("recipient@example.com");
msg.setSubject("Hello");
msg.addMessage({ contentType: "text/html", data: "<h1>Hello</h1>" });
await env.EMAIL.send(new EmailMessage("sender@yourdomain.com", "recipient@example.com", msg.asRaw()));
```
Requires `npm install mimetext` and `"nodejs_compat"` in compatibility flags.
## Agents SDK Email
Agents can receive and reply to emails natively via the Agents SDK.
```jsonc
// wrangler.jsonc
{
"durable_objects": {
"bindings": [{ "name": "EmailAgent", "class_name": "EmailAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["EmailAgent"] }],
"send_email": [{ "name": "EMAIL", "destination_address": "reply@yourdomain.com" }]
}
```
```typescript
import { Agent } from "agents";
import { type AgentEmail } from "agents/email";
import PostalMime from "postal-mime";
export class EmailAgent extends Agent<Env, State> {
async onEmail(email: AgentEmail) {
const parsed = await PostalMime.parse(await email.getRaw());
await this.replyToEmail(email, {
fromName: "My Agent",
subject: `Re: ${parsed.subject}`,
body: "Thanks for your email! I'll look into this.",
});
}
}
```
Route emails to agents with resolvers:
```typescript
import { routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
export default {
async email(message, env) {
await routeAgentEmail(message, env, {
resolver: createAddressBasedEmailResolver("EmailAgent"),
});
},
};
```
Resolver types: `createAddressBasedEmailResolver` (recipient → instance name), `createSecureReplyEmailResolver(secret)` (HMAC-signed replies), `createCatchAllEmailResolver("Agent", "default")` (single inbox). Use `isAutoReplyEmail(email.headers)` to skip vacation responders.
## Error Handling
```typescript
try {
const response = await env.EMAIL.send({ /* ... */ });
} catch (error) {
// error.code is one of the E_* error codes
console.error(`Failed: ${error.code} - ${error.message}`);
}
```
These error codes are for the **Workers binding** (thrown as Error objects with `.code` and `.message`). The **REST API** returns standard Cloudflare API numeric error codes instead — see [rest-api.md](rest-api.md).
| Error Code | What It Means | What to Do |
|------------|---------------|------------|
| `E_VALIDATION_ERROR` | Invalid payload | Check email format, required fields |
| `E_FIELD_MISSING` | Required field missing | Add `to`, `from`, or `subject` |
| `E_TOO_MANY_RECIPIENTS` | Combined to/cc/bcc exceeds 50 | Split into multiple sends |
| `E_SENDER_NOT_VERIFIED` | Domain not onboarded | Run `wrangler email sending enable yourdomain.com` or onboard in Dashboard |
| `E_RECIPIENT_NOT_ALLOWED` | Recipient not in allowed list | Add to `allowed_destination_addresses` |
| `E_RECIPIENT_SUPPRESSED` | Address bounced or reported spam | Remove from your list; check suppression list in Dashboard |
| `E_SENDER_DOMAIN_NOT_AVAILABLE` | Domain not available for sending | Complete domain onboarding |
| `E_CONTENT_TOO_LARGE` | Content exceeds 25 MiB | Reduce attachments or body |
| `E_RATE_LIMIT_EXCEEDED` | Rate limit hit | Retry with exponential backoff |
| `E_DAILY_LIMIT_EXCEEDED` | Daily quota reached | Wait or request limit increase |
| `E_DELIVERY_FAILED` | SMTP delivery failure | Check recipient address, retry if transient |
| `E_INTERNAL_SERVER_ERROR` | Service temporarily unavailable | Retry with exponential backoff |
| `E_HEADER_NOT_ALLOWED` | Header not on whitelist | Use an allowed header; see [headers reference](https://developers.cloudflare.com/email-service/reference/headers/) |
| `E_HEADER_USE_API_FIELD` | Must use API field instead | Set `From`, `To`, etc. via the dedicated API fields, not `headers` |
| `E_HEADER_VALUE_INVALID` | Header value is malformed or empty | Fix the value format (e.g., List-Unsubscribe needs angle-bracket URIs) |
| `E_HEADER_VALUE_TOO_LONG` | Header value exceeds 2,048 bytes | Shorten the header value |
| `E_HEADER_NAME_INVALID` | Invalid header name | Fix characters or keep under 100 bytes |
| `E_HEADERS_TOO_LARGE` | Total headers exceed 16 KB | Reduce number or size of custom headers |
| `E_HEADERS_TOO_MANY` | More than 20 non-X headers | Reduce to 20 or fewer whitelisted headers |
For `E_RATE_LIMIT_EXCEEDED` and `E_DELIVERY_FAILED`, retry with exponential backoff. For validation errors (`E_VALIDATION_ERROR`, `E_FIELD_MISSING`, `E_SENDER_NOT_VERIFIED`), fix the request — retrying won't help.
## Restricted Bindings
Restrict which `from` addresses a binding can use:
```jsonc
{
"send_email": [{
"name": "RESTRICTED_EMAIL",
"allowed_sender_addresses": ["noreply@yourdomain.com", "support@yourdomain.com"]
}]
}
```
@@ -0,0 +1,110 @@
---
name: cloudflare-one-migrations
description: Plans migrations from Zscaler ZIA/ZPA, Palo Alto, legacy VPN, SWG, or SASE stacks to Cloudflare One. Use for migration assessments, policy mapping, rollout plans, and parity/gap analysis.
---
# Cloudflare One Migrations
Retrieve current Cloudflare docs, Cloudflare API schemas, and source-vendor export docs before generating exact configuration.
## Workflow
1. Identify the source stack: Zscaler ZIA, Zscaler ZPA, Palo Alto NGFW/Prisma/GlobalProtect, legacy VPN/SWG/SD-WAN, or other.
2. Request exports and logs before mapping. Prefer structured exports over screenshots or prose summaries.
3. Build an inventory: identities, groups, apps, destinations, connectors/tunnels, DNS/URL/firewall/DLP/TLS policies, objects/lists, locations/sites, exceptions, hit counts, and compliance logging.
4. Produce a mapping plan: source object, Cloudflare One target resource, confidence, prerequisites, unsupported/partial mappings, and manual decisions.
5. Create dependencies first: identity/[SCIM](https://developers.cloudflare.com/cloudflare-one/team-and-resources/users/scim/), connectors/on-ramps, routes/DNS, lists/objects, TLS bypasses, Access apps/policies, Gateway policies, DLP/CASB, logging.
6. Stage safely: use a migration prefix, create disabled/audit-mode rules by default, pilot with small groups/sites, compare logs, then expand rollout.
7. Account for every source rule. Each rule must map to a Cloudflare object or an explicit Not Migrated row with reason and security impact.
## Exports To Ask For
- ZIA: URL filtering, firewall filtering, SSL inspection, DLP, custom URL categories, IP groups, network services/service groups, users/groups/departments, locations, GRE tunnels, and static IPs.
- ZPA: app segments, segment groups, server groups, app connectors/connector groups, access policies, IdP/group mapping, private DNS domains, ports, and protocols.
- Palo Alto/Prisma: security/NAT/decryption rules, address/service objects and groups, URL categories, HIP profiles, GlobalProtect config, Prisma Access remote network/service connection config, zones, tags, logs, and hit counts.
## Mapping Heuristics
- ZIA/SWG policies usually map to [Gateway traffic policies](https://developers.cloudflare.com/cloudflare-one/traffic-policies/) and Gateway lists.
- ZPA private app access usually maps to [Access application types](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/choose-application-type/), [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/), private network routing/DNS, and [Access policies](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/).
- Palo Alto rules map only after understanding traffic direction, zones, objects, users, apps, decryption, and hit counts. Do not flatten zones blindly into lists.
- Legacy VPN replacement is usually Access + Cloudflare One Client / WARP + Tunnel or Mesh for app access. Use [Cloudflare WAN](https://developers.cloudflare.com/cloudflare-wan/) only when site-to-site traffic is required; use the [Network VPN migration design guide](https://developers.cloudflare.com/reference-architecture/design-guides/network-vpn-migration/) and [Replace your VPN](https://developers.cloudflare.com/cloudflare-one/setup/replace-vpn/) docs for current patterns.
## Migration Assessment Prompts
- Source coverage: which products are in scope, which exports are available, and whether screenshots/prose summaries are hiding missing object files.
- Rule volume and hit data: counts by rule type, disabled/stale rules, no-hit rules, high-hit rules, and business-critical exceptions.
- Object dependencies: address objects, service objects, groups, custom categories, network services, app IDs, zones, tags, connectors, and server groups.
- Identity readiness: IdP, SCIM/group sync, group-name normalization, individual-user rules, local groups, service accounts, and contractor identities.
- TLS/DLP readiness: source decryption rules, certificate-pinned bypasses, [DLP](https://developers.cloudflare.com/cloudflare-one/data-loss-prevention/) engines/profiles, custom regex, exact-match data, and payload logging expectations.
- Connectivity readiness: source tunnels/connectors, private DNS, [Split Tunnels](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/route-traffic/split-tunnels/) or bypass behavior, source IP preservation, [egress IP](https://developers.cloudflare.com/cloudflare-one/traffic-policies/egress-policies/) allowlists, and site-to-site requirements.
- Rollout readiness: pilot groups/sites, parallel-run period, rollback owner, source-stack decommission criteria, and monitoring/log comparison plan.
## Source-Specific Traps
### Zscaler ZIA / SWG
- Custom URL categories often split into separate IP, domain, and URL lists. Count the generated lists, not just source categories.
- ZIA locations with IPs are useful as source IP lists; they are not automatically [Gateway DNS locations](https://developers.cloudflare.com/cloudflare-one/networks/resolvers-and-proxies/dns/locations/) for DNS policy scoping.
- GRE tunnel source IPs can inform policy conditions, but the transport migration is a separate WARP Connector or Cloudflare WAN workstream.
- CAUTION/warn behavior has no exact Gateway equivalent. Treat it as an explicit customer decision, not a silent allow/block choice.
- DLP engines and custom regex usually require manual Cloudflare DLP profile recreation. Placeholder policies must not be enabled as if DLP is complete.
- Network application groups and unsupported protocols are partial mappings. Review them before enablement.
- If SCIM is unavailable, identity-scoped source rules become overly broad unless you add an enforceable alternative such as user/email lists. Check [Gateway identity selectors](https://developers.cloudflare.com/cloudflare-one/traffic-policies/identity-selectors/) before creating those rules.
### Zscaler ZPA / Private Access
- ZPA app segments, server groups, and connector groups do not map 1:1. Cloudflare separates Access apps, tunnel routes, DNS, and policies.
- Creating tunnels through the API does not complete connector deployment. Plan cloudflared installation, authentication, and origin reachability separately.
- Create one Cloudflare Tunnel per ZPA connector group regardless of connector runtime status (AUTHENTICATED, DISCONNECTED, or disabled). Status is operational, not architectural. Tag disconnected or legacy groups in the tunnel description and let the customer decide what to decommission after validation.
- Each ZPA connector instance within a group maps to one cloudflared replica running against that tunnel's token. Match replica count to connector instance count per group to preserve the same topology. A single tunnel token supports multiple simultaneous cloudflared processes. Recommend installing replicas within the same data center but on different hosts or subnets.
- For each connector group, identify all server groups linked to it and all app segments assigned to those server groups. IP addresses and CIDRs in those app segments become CIDR routes on the corresponding tunnel; domain names become hostname routes on the same tunnel. Prefer one CIDR route per subnet over per-host /32 routes where a broad subnet covers all app segment IPs.
- ZPA bypass means split-tunnel bypass in Cloudflare, not an Access `bypass` decision. Bypass rules map to WARP [Split Tunnel](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/route-traffic/split-tunnels/) exclude entries. This is a manual configuration step with no API automation - the customer must add bypassed domains and IPs to the device profile split tunnel exclude list through the dashboard.
- Agentless/browser apps may become separate public-hostname Access apps per domain. WARP private apps remain private-destination apps.
- The default Cloudflare Access application destination limit is 5 hostnames per app. For ZPA migrations with large app segments, contact the Cloudflare account team to request an increase (up to 50) before implementation. Confirm the limit is active on the account before creating apps - without it, large segments must be split into multiple apps with identical policies, significantly increasing object count.
- IP-anchored apps require an explicit egress decision before migration: preserve source IP through customer egress, use Cloudflare [dedicated egress](https://developers.cloudflare.com/cloudflare-one/traffic-policies/egress-policies/) where available, or accept that the target service must be updated to allow new source IPs. This is a customer decision that blocks implementation if unresolved.
- Resolver policies can be account-wide. Be careful with overlapping private DNS namespaces across sites or virtual networks; retrieve [resolver policy](https://developers.cloudflare.com/cloudflare-one/traffic-policies/resolver-policies/) docs before making DNS changes.
- Each ZPA access policy rule maps to a Cloudflare reusable Access policy. Create all reusable policies before attaching them to Access apps. In default-deny Gateway Network environments, additionally create a Network allow rule with selector "Self-hosted Access App with Private Address is Present" (wirefilter: `any(access.private_app[*] in {"*"})`) at higher precedence than any broad L4 block rules - without it, Gateway blocks private app traffic before Access policy evaluation occurs.
- In combined ZIA and ZPA migrations, Gateway Network rules can accidentally block Access private-app traffic. The Gateway Network allow rule above is the fix - place it at higher precedence (lower number) than ZIA-migrated block rules. Add and validate this rule before enabling broad L4 blocks.
### Palo Alto / Prisma / NGFW
- One Palo Alto rule can produce multiple Cloudflare resources. Preserve rule intent, not rule count.
- App-ID, URL category, zone, HIP, schedule, and decryption behavior rarely translate exactly. Mark partial mappings rather than forcing false equivalence.
- Export address/service objects and groups with rules. Missing object exports cause silent-looking drops unless explicitly detected.
- Broad `any` destination/service rules and very broad CIDRs require manual review. Do not auto-create broad catchalls.
- HIP/device checks require Cloudflare [device posture](https://developers.cloudflare.com/cloudflare-one/reusable-components/posture-checks/) integrations before enforcement.
## Gotchas
- Source exports often split references across files. Resolve IDs against object, service, and group files before declaring a rule unmappable.
- Individual users, local groups, departments, and dynamic application IDs often need identity normalization. SCIM/group sync is the gating prerequisite for group selectors.
- Zscaler caution/warn behavior, Palo Alto App-ID behavior, and TLS/decryption exceptions may not have exact equivalents. Flag them as decision points instead of forcing a 1:1 mapping.
- Preserve source rule order and hit counts where available. Disable or delete stale/no-hit rules only with user approval.
- Never create broad allow-all catchalls to preserve connectivity unless explicitly requested and time-limited.
## Validation Gates
- After each migration stage, compare Cloudflare object counts against parsed source counts. Stop on mismatches.
- Review every `unsupported`, `partial`, `unmapped`, `needs_identity`, `needs_posture`, and `manual_review` item before enabling policies.
- Validate group matching with real pilot users after SCIM sync and re-authentication.
- Test TLS inspection and Do Not Inspect behavior before enabling HTTP/DLP blocks broadly.
- Keep rollback paths explicit: disable migrated rules by prefix, restore source routing, or revert the pilot group/site.
- Before declaring done, produce a source-rule accounting table: migrated object, partial mapping, not migrated reason, security impact, and owner for each manual action.
## Assessment Template
```markdown
## Migration Assessment
Source stack:
Artifacts reviewed:
Assumptions / missing exports:
Recommended Cloudflare One target:
Mapping summary:
Risks / partial mappings:
Not migrated:
Pilot plan:
Validation:
Rollback:
```
+176
View File
@@ -0,0 +1,176 @@
---
name: cloudflare-one
description: "Guides Cloudflare One Zero Trust and SASE work across Access, Gateway, WARP, Tunnel, Cloudflare WAN, DLP, CASB, device posture, and identity. Use when designing, configuring, troubleshooting, or reviewing Cloudflare One deployments. Retrieval-first: use current Cloudflare docs/API schemas instead of embedded product docs."
---
# Cloudflare One
Before citing limits, settings, API fields, category IDs, or exact UI paths, retrieve current information from the [Cloudflare One docs](https://developers.cloudflare.com/cloudflare-one/), the Cloudflare docs MCP server, or the Cloudflare API schema.
## Workflow
1. Classify the ask: architecture, configuration, troubleshooting, migration, or review.
2. Gather context: account ID, users/sites/apps, identity provider, SCIM/group sync, device management, traffic path, compliance constraints, and rollout blast radius.
3. Retrieve only the current docs needed for the products involved: Access, Gateway, WARP/device client, Tunnel/Mesh, Cloudflare WAN, DLP, CASB, device posture, or identity.
4. If account access is available, inspect existing resources before proposing or making changes: Access apps/policies/groups/IdPs, Gateway rules/lists/categories, device profiles/posture checks, tunnels/routes, DNS/resolver settings, and locations/sites.
5. Propose the change set with prerequisites, validation, and rollback. For risky changes, stage disabled or scoped to a pilot group/site unless the user explicitly asks otherwise.
## Assessment Prompts
Use these to avoid jumping straight to configuration. Ask only the prompts relevant to the user's task.
### Architecture and Current State
- Sites and users: offices, branches, data centers, VPCs, remote users, contractors, user counts, and current connectivity model.
- Applications and destinations: SaaS, public apps, private apps, APIs, infrastructure targets, protocols, ports, hostnames, and IP ranges.
- Connectivity: VPN, MPLS, SD-WAN, direct Internet breakout, centralized backhaul, site-to-site needs, and private DNS architecture.
- Security stack: current SWG, NGFW, VPN/ZTNA, DLP, CASB, email security, logging, and compliance requirements.
- Identity: IdP, SCIM/group sync, group naming, multi-IdP needs, service accounts, and contractor/partner access.
- Rollout: pilot users/sites, blast radius, rollback path, support owners, and success criteria.
### Access and SaaS Federation
- App shape: web app, API, SSH/RDP/VNC, database, SaaS app, public hostname, private IP, or private hostname. Retrieve [Access application type](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/choose-application-type/) docs before choosing.
- Access model: clientless browser access, private networking with device client, peer to peer connectivity, service connections with service tokens or mutual TLS, or SaaS SSO federation.
- Policy needs: user groups, device posture, session duration, mTLS, service tokens, and app launcher visibility. Retrieve [Access policy](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/) docs before configuring selectors or evaluation order.
- SaaS details: SAML vs OIDC support, ACS/redirect URLs, Entity IDs/client IDs, required attributes, and tenant-control requirements.
### Tunnel and Private Networking
- Sites and segments: which data centers, VPCs, offices, or network segments need connectivity.
- HA: dev/test single connector, production multiple connectors, or advanced multi-tunnel/site redundancy.
- Runtime: where cloudflared or WARP Connector/Mesh will run: VM, container, Kubernetes, bare metal, or other target.
- Egress: whether connectors can reach Cloudflare over the required outbound ports/protocols. Retrieve [Tunnel connectivity prechecks](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/troubleshoot-tunnels/connectivity-prechecks/) before naming exact endpoints.
- Origin reachability: whether the connector can resolve and reach every private origin.
- Routing: required CIDRs/hostnames, overlapping IP spaces, [virtual networks](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/cloudflared/tunnel-virtual-networks/), [Split Tunnels](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/route-traffic/split-tunnels/), and private DNS/[resolver policy](https://developers.cloudflare.com/cloudflare-one/traffic-policies/resolver-policies/) needs.
- Management model: prefer remotely managed/token-based tunnels for new deployments unless there is a clear reason for local config.
### Gateway, TLS, and DLP
- Traffic controls: DNS categories, HTTP URL/path inspection, L4 ports/protocols, egress IP requirements, custom lists, and allow/block exceptions. Retrieve [Gateway traffic policy](https://developers.cloudflare.com/cloudflare-one/traffic-policies/) docs for current selectors and order of enforcement.
- Identity: whether Gateway policies need user or group selectors, and whether users will be authenticated through WARP/IdP context. Check [Gateway identity selectors](https://developers.cloudflare.com/cloudflare-one/traffic-policies/identity-selectors/) and [SCIM provisioning](https://developers.cloudflare.com/cloudflare-one/team-and-resources/users/scim/) when groups are involved.
- TLS inspection: root CA deployment path, certificate-pinned applications, compliance exceptions, and FIPS requirements. Retrieve [TLS decryption](https://developers.cloudflare.com/cloudflare-one/traffic-policies/http-policies/tls-decryption/) docs before enabling.
- DLP: sensitive data types, channels to inspect, TLS inspection readiness, DLP profiles, payload logging requirements, and false-positive tolerance. Retrieve [DLP](https://developers.cloudflare.com/cloudflare-one/data-loss-prevention/) docs before creating enforcement.
### CASB, Device Posture, and Risk
- CASB: SaaS vendors, admin access level, scan policy, org size, remediation owner, and whether inline protection is also required. Retrieve [CASB findings](https://developers.cloudflare.com/cloudflare-one/cloud-and-saas-findings/manage-findings/) docs before recommending remediation.
- Device posture: required checks, third-party EDR/MDM integrations, enrollment rules, device profiles, and split tunnel alignment.
- Risk scoring: relevant behavior signals, false-positive sources such as VPNs or service accounts, and whether risk is for investigation or enforcement. Retrieve [user risk score](https://developers.cloudflare.com/cloudflare-one/team-and-resources/users/risk-score/) docs before using risk in policies.
### Cloudflare WAN / Site Connectivity
- Site topology, on-ramp type, route ownership, tunnel redundancy, static vs BGP-managed routes, network firewall needs, and appliance/profile ownership. Retrieve [Cloudflare WAN](https://developers.cloudflare.com/cloudflare-wan/) and [Cloudflare Network Firewall](https://developers.cloudflare.com/cloudflare-network-firewall/) docs before proposing site connectivity changes.
## Guardrails
- Access controls application authorization; Gateway controls traffic inspection/filtering. Use both when the requirement spans identity-aware app access and network/web security.
- Public hostname Access apps can be clientless. Private destination apps require WARP/Device client or another network on-ramp plus routes and DNS resolution. Retrieve [self-hosted private app](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/non-http/self-hosted-private-app/) docs before configuring private destinations.
- Cloudflare Tunnel is an off-ramp from a private network to Cloudflare. Cloudflare WAN and Mesh are other off-ramps which can also be on-ramps.
- Group-based policies depend on IdP group claims or SCIM. If group sync is missing, do not invent group selectors.
- Private hostnames need explicit DNS routing/resolution; creating an Access app alone is not enough. Use [resolver policies](https://developers.cloudflare.com/cloudflare-one/traffic-policies/resolver-policies/) and review [Connect a private hostname](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/private-net/cloudflared/connect-private-hostname/)
- HTTP inspection and DLP for encrypted web traffic require TLS inspection and planned Do Not Inspect exceptions.
- Gateway DNS, Network, HTTP, and Egress policies have different evaluation semantics. Retrieve [order of enforcement](https://developers.cloudflare.com/cloudflare-one/traffic-policies/order-of-enforcement/) docs before explaining precedence.
- Start broad block/allow/DLP/TLS policies disabled limited to a pilot with specific target users or groups unless the user approves a wider rollout.
### Identity and Access
- Access Groups are Cloudflare objects; IdP/SCIM groups are identity claims. Gateway group selectors use synced IdP groups, not Access Groups.
- Group names and SAML/OIDC attributes are case-sensitive. Verify exact claim names and values before creating group-based rules.
- SCIM changes and group membership can be stale until sync and re-authentication complete. Troubleshoot with the user's last authenticated identity, not just the IdP state.
- Access policies are default-deny. A private app with routes but no Allow policy still blocks access.
- Access policy selectors can use IP lists, not Gateway domain or URL lists.
- SaaS federation handles authentication into the SaaS app. SaaS authorization and tenant restrictions usually require SaaS-side roles and/or Gateway tenant controls.
- Browser Rendering for SSH/VNC/RDP is an Access capability. Browser Isolation renders general web content remotely. Do not conflate them.
### Device Client Deployment
- The [Cloudflare One device client](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/) is the on-ramp for user devices. Two components control it: **enrollment rules** (who can connect) and **device profiles** (how the client behaves after enrollment).
- The enrollment rule is an Access application of type `warp`, not a device setting. It accepts reusable Access policies. Look in Access for enrollment debugging, not Devices.
- For headless or autonomous devices (services, kiosks, Linux hosts), use service token enrollment. Non-human devices authenticate as `non_identity@[team-domain].cloudflareaccess.com` and have no group membership - device profiles targeting IdP groups will not match them. Target headless devices explicitly with the non-identity email, specific conventions about the devices (OS information, etc.),or let them fall to the default profile.
- Device profiles control connection mode, [split tunnel](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/route-traffic/split-tunnels/) configuration, user permissions (disable, switch lock), auto-reconnect, and captive portal behavior. Profiles are matched by user group or device attributes in precedence order - first match wins, default profile catches the rest.
- Split tunnel mode is the single most impactful client setting. Choose the mode based on the deployment goal:
| Goal | Mode | Rationale |
|---|---|---|
| VPN replacement only (private apps) | **Include** | Route only specified private CIDRs and hostnames through the client. Everything else goes direct. Minimal blast radius. |
| SWG only (internet security) | **Exclude** | All traffic through the client. Exclude only what breaks (local printers, certificate-pinned apps). |
| VPN replacement + SWG | **Exclude** | All traffic through the client. Most common enterprise configuration. |
| Coexistence with another VPN | **Include** | Avoids conflict with the other VPN's tunnel interface and DNS control. |
| DNS filtering only | DNS-only mode | Only DNS queries go to Gateway. No traffic proxying. |
- Include vs exclude is per-profile, not per-entry. You cannot mix modes in the same profile. Switching modes mid-deployment requires re-evaluating every entry.
- Split tunnel entries must align with tunnel routes bidirectionally. A CIDR in the include list without a matching tunnel route causes a black hole. A tunnel route without a matching device profile entry means traffic never enters the tunnel.
- MDM parameters (`mdm.xml` / managed preferences) override dashboard-configured profile settings for any setting specified in the file. If dashboard changes appear to have no effect on managed devices, check MDM config. Retrieve [MDM deployment](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/deployment/mdm-deployment/) docs for platform-specific file locations and parameters.
- If another VPN client or agent controls DNS on the device, the device client's DNS interception will conflict. In coexistence scenarios, use "traffic only" mode to avoid routing table and DNS conflicts.
- Captive portal detection temporarily disconnects the client when it detects a portal (hotel WiFi, airport). This is a common source of end-user friction and should be managed carefully.
### Private Networking
- Split tunnel mode changes the meaning of every route decision: Exclude mode sends traffic to Cloudflare when removed from excludes; Include mode sends traffic only when added to includes.
- Virtual networks should be used primarily when IP subnets overlap and hostname-based routing is not used. It can be used to control other user connectivity behavior, but it is recommended to manage through security policies.
- A healthy tunnel only proves cloudflared can reach Cloudflare. The tunnel must have appropriate published application routes, network routes, or hostname routes for connectivity to function.
- Cloudflare Tunnel and Cloudflare Mesh can both be used to facilitate connectivity to internal networks. Cloudflare WAN can as well, but it is gated behind Enterprise subscriptions. Retrieve [choose an on-ramp](https://developers.cloudflare.com/learning-paths/secure-internet-traffic/connect-devices-networks/choose-on-ramp/) when deliberating between Tunnel types.
- Run multiple cloudflared connectors for production HA, preferably on separate hosts. Token-based, remotely managed tunnels are the default for new deployments.
### Gateway, TLS, and DLP
- `dns.domains` matches a domain and subdomains; `dns.fqdn` is exact-match only.
- DNS pre-resolution selectors and post-resolution selectors do not behave like a single strict precedence list. Retrieve current evaluation docs before changing rule order.
- HTTP Do Not Inspect rules run before HTTP Allow/Block/Isolate behavior. A later block rule will not override an earlier inspection bypass.
- Certificate-pinned apps need Do Not Inspect exceptions before broad TLS inspection. Deploy the Cloudflare root CA to managed devices before enabling inspection.
- DLP profiles are detection definitions only. They do nothing until referenced by Gateway HTTP policies or CASB scan settings. Rules with body inspection may be evaluated multiple times in a single pass.
- Start DLP with payload logging where appropriate, tune false positives, then block.
- Gateway Network policies are strict L4 controls. Identity-aware L4 matching requires authenticated device context.
### CASB, Risk, and Operations
- API CASB is out-of-band and periodic. It does not provide real-time inline enforcement although some integrations support "remediation"; use Gateway granular application controls for inline CASB capability for supported applications. Retrieve [Granular application controls](https://developers.cloudflare.com/cloudflare-one/traffic-policies/http-policies/granular-controls/) when creating security policies for specific actions in specific SaaS applications.
- CASB findings are tied to specific assets and instances. Drill into affected assets before recommending remediation.
- Use current Dashboard remediation guidance for CASB fixes. Most remediations happen in the SaaS admin console, not Cloudflare.
- Large SaaS integrations can take 24-48 hours for initial scans. Reauthorizing can restart scan state; check credential health before reconnecting.
- User risk scores are behavior-based and asynchronous. CASB findings do not automatically imply high user risk.
### Infrastructure Access
- [Zero Trust Infrastructure Access](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/non-http/infrastructure-apps/) (ZTIA) is the purpose-built offering for SSH access through the device client. It provides capabilities not available through self-hosted apps: keystroke logging, control over how users authenticate to the target machine, [short-lived certificates](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/ssh/ssh-infrastructure-access/#generate-a-cloudflare-ssh-ca) that replace static SSH keys with ephemeral certs tied to Access identity, and lightweight privileged access management. Use Infrastructure Access apps for SSH when the device client is deployed.
- [Browser Rendering](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/non-http/browser-rendering/) provides clientless SSH, RDP, and VNC through the browser without requiring the device client. Clientless RDP includes session recording and file transfer controls. Use clientless access when a device client cannot be installed (contractors, partner access, unmanaged devices) - typically not as the default for managed users with the client installed.
- [Audit SSH](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/use-cases/ssh/ssh-infrastructure-access/#enable-ssh-command-logging) is a Gateway Network policy action that logs SSH commands without blocking. It requires the session to be proxied through Cloudflare.
- Short-lived certificates require CA configuration on the target host and `sshd` configured to trust the Cloudflare CA public key. Retrieve [short-lived certificate setup](https://developers.cloudflare.com/cloudflare-one/identity/users/short-lived-certificates/) docs before configuring.
- For kubectl and database access behind private networks, use the device client with private destination routing. There is no Infrastructure Access or browser-rendered equivalent for arbitrary TCP protocols today.
### Logs, Analytics, and DEX
- [Gateway activity logs](https://developers.cloudflare.com/cloudflare-one/analytics/logs/gateway-logs/) record DNS, HTTP, and Network policy decisions. Filter by rule name, user identity, destination, action, and time range. These are the primary troubleshooting tool for "why was this blocked/allowed."
- [Access audit logs](https://developers.cloudflare.com/cloudflare-one/insights/logs/dashboard-logs/access-authentication-logs/) record authentication decisions per app - who authenticated, which policy matched, and session details. Use for verifying policy behavior and investigating access failures.
- [Shadow IT discovery](https://developers.cloudflare.com/cloudflare-one/insights/analytics/shadow-it-discovery/) uses Gateway HTTP logs to surface unmanaged SaaS applications. Requires TLS inspection for HTTPS visibility.
- [DEX (Digital Experience Monitoring)](https://developers.cloudflare.com/cloudflare-one/insights/dex/) provides fleet-level and per-device connectivity diagnostics. Use [DEX tests](https://developers.cloudflare.com/cloudflare-one/insights/dex/tests/) (HTTP, traceroute) to proactively monitor reachability to critical origins and internal apps. Fleet status shows device client health, connection mode, and connectivity state across the enrolled population.
- [Logpush](https://developers.cloudflare.com/cloudflare-one/analytics/logs/logpush/) exports Gateway, Access, Network, and DEX logs to external SIEM or storage. Configure before go-live if the customer requires centralized log retention or compliance reporting.
- When troubleshooting, work from logs toward config: identify the log entry showing the failure (Gateway block, Access deny, tunnel error, DNS resolution miss), then trace back to the responsible rule, route, or policy.
### Cloudflare WAN / Site Connectivity
- Cloudflare WAN is connectivity, not a security service. Apply inspection and policy with Gateway and Network Firewall where required.
- WAN firewall expressions are not the same language as Gateway wirefilter expressions. Retrieve the current syntax before editing.
- Generated IPsec PSKs and some OAuth/client secrets are returned once. Store them immediately.
## Output Defaults
- Designs: current assumptions, target architecture, product responsibilities, rollout phases, validation, and open decisions.
- Configuration work: prerequisites, exact resources to inspect/create/change, test cases, and rollback.
- Troubleshooting: traffic path, likely failure point, evidence to collect, and next test.
## Validation Prompts
- Access: test authorized, unauthorized, posture-failing, service-token, and multi-IdP flows when applicable; inspect logs and policy precedence.
- Private network access: verify route lookup, tunnel health, origin reachability, split tunnel behavior, DNS resolution, and end-to-end access from a device client test device.
- Gateway: verify rule type, action, traffic expression, precedence/evaluation phase, referenced lists, and Gateway settings before enabling broadly.
- TLS/DLP: test Do Not Inspect exceptions and root CA trust before enabling inspection; test DLP with known samples and monitor false positives before blocking.
- CASB/risk: confirm integration health, credential expiry, asset discovery, scan timing, finding instances, and risk-score signal latency before declaring remediation complete.
- Cloudflare WAN: verify tunnel health, route priority/ownership, traffic flow, firewall expression syntax, and connector/appliance telemetry where applicable.
## API Safety
- Use fully qualified MCP tool names when MCP tools are available.
- Never guess category IDs, application IDs, wirefilter fields, or API request bodies. Retrieve the current schema/docs and existing account objects.
- Do not enable broad production policies without explicit approval.
+248
View File
@@ -0,0 +1,248 @@
---
name: cloudflare
description: Comprehensive Cloudflare platform skill covering Workers, Pages, storage (KV, D1, R2), AI (Workers AI, Vectorize, Agents SDK), feature flags (Flagship), networking (Tunnel, Spectrum), security (WAF, DDoS), and infrastructure-as-code (Terraform, Pulumi). Use for any Cloudflare development task. Biases towards retrieval from Cloudflare docs over pre-trained knowledge.
references:
- workers
- pages
- d1
- durable-objects
- workers-ai
---
# Cloudflare Platform Skill
Consolidated skill for building on the Cloudflare platform. Use decision trees below to find the right product, then load detailed references.
Your knowledge of Cloudflare APIs, types, limits, and pricing may be outdated. **Prefer retrieval over pre-training** — the references in this skill are starting points, not source of truth.
## Retrieval Sources
Fetch the **latest** information before citing specific numbers, API signatures, or configuration options. Do not rely on baked-in knowledge or these reference files alone.
| Source | How to retrieve | Use for |
|--------|----------------|---------|
| Cloudflare docs | `cloudflare-docs` search tool or `https://developers.cloudflare.com/` | Limits, pricing, API reference, compatibility dates/flags |
| Workers types | `npm pack @cloudflare/workers-types` or check `node_modules` | Type signatures, binding shapes, handler types |
| Wrangler config schema | `node_modules/wrangler/config-schema.json` | Config fields, binding shapes, allowed values |
| Product changelogs | `https://developers.cloudflare.com/changelog/` | Recent changes to limits, features, deprecations |
When a reference file and the docs disagree, **trust the docs**. This is especially important for: numeric limits, pricing tiers, type signatures, and configuration options.
## Quick Decision Trees
### "I need feature flags"
```
Need feature flags?
└─ Feature toggles, targeting rules, percentage rollouts → flagship/
├─ Evaluate in Workers → Flagship binding (env.FLAGS)
├─ Evaluate in Node.js / browser → OpenFeature SDK (@cloudflare/flagship)
└─ Manage flags via API → Flagship REST API
```
### "I need to run code"
```
Need to run code?
├─ Serverless functions at the edge → workers/
├─ Full-stack web app with Git deploys → pages/
├─ Stateful coordination/real-time → durable-objects/
├─ Long-running multi-step jobs → workflows/
├─ Run containers → containers/
├─ Multi-tenant (customers deploy code) → workers-for-platforms/
├─ Scheduled tasks (cron) → cron-triggers/
├─ Lightweight edge logic (modify HTTP) → snippets/
├─ Process Worker execution events (logs/observability) → tail-workers/
└─ Optimize latency to backend infrastructure → smart-placement/
```
### "I need to store data"
```
Need storage?
├─ Key-value (config, sessions, cache) → kv/
├─ Relational SQL → d1/ (SQLite) or hyperdrive/ (existing Postgres/MySQL)
├─ Object/file storage (S3-compatible) → r2/
├─ Versioned file trees (repos, build outputs, checkpoints) → artifacts/
├─ Message queue (async processing) → queues/
├─ Vector embeddings (AI/semantic search) → vectorize/
├─ Strongly-consistent per-entity state → durable-objects/ (DO storage)
├─ Secrets management → secrets-store/
├─ Streaming ETL to R2 → pipelines/
├─ Managed Apache Iceberg catalog on R2 → r2-data-catalog/
├─ Serverless SQL analytics over Iceberg tables → r2-sql/
└─ Persistent cache (long-term retention) → cache-reserve/
```
### "I need AI/ML"
```
Need AI?
├─ Run inference (LLMs, embeddings, images) → workers-ai/
├─ Vector database for RAG/search → vectorize/
├─ Build stateful AI agents → agents-sdk/
├─ Gateway for any AI provider (caching, routing) → ai-gateway/
└─ AI-powered search widget → ai-search/
```
### "I need networking/connectivity"
```
Need networking?
├─ Expose local service to internet → tunnel/
├─ TCP/UDP proxy (non-HTTP) → spectrum/
├─ WebRTC TURN server → turn/
├─ Private network connectivity → network-interconnect/
├─ Optimize routing → argo-smart-routing/
├─ Optimize latency to backend (not user) → smart-placement/
└─ Real-time video/audio → realtimekit/ or realtime-sfu/
```
### "I need security"
```
Need security?
├─ Web Application Firewall → waf/
├─ DDoS protection → ddos/
├─ Bot detection/management → bot-management/
├─ API protection → api-shield/
├─ CAPTCHA alternative → turnstile/
└─ Credential leak detection → waf/ (managed ruleset)
```
### "I need media/content"
```
Need media?
├─ Image optimization/transformation → images/
├─ Video streaming/encoding → stream/
├─ Browser automation/screenshots → browser-rendering/
└─ Third-party script management → zaraz/
```
### "I need analytics/metrics data"
```
Need analytics?
├─ Query across all Cloudflare products (HTTP, Workers, DNS, etc.) → graphql-api/
├─ Custom high-cardinality metrics from Workers → analytics-engine/
├─ Client-side (RUM) performance data → web-analytics/
├─ Workers Logs and real-time debugging → observability/
├─ SQL over Iceberg data lake (logs, events) → r2-sql/ (+ pipelines/, r2-data-catalog/)
└─ Raw logs (Logpush to external tools) → Cloudflare docs
```
### "I need infrastructure-as-code"
```
Need IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API)
```
## Product Index
### Feature Flags
| Product | Reference |
|---------|-----------|
| Flagship | `references/flagship/` |
### Compute & Runtime
| Product | Reference |
|---------|-----------|
| Workers | `references/workers/` |
| Pages | `references/pages/` |
| Pages Functions | `references/pages-functions/` |
| Durable Objects | `references/durable-objects/` |
| Workflows | `references/workflows/` |
| Containers | `references/containers/` |
| Workers for Platforms | `references/workers-for-platforms/` |
| Cron Triggers | `references/cron-triggers/` |
| Tail Workers | `references/tail-workers/` |
| Snippets | `references/snippets/` |
| Smart Placement | `references/smart-placement/` |
### Storage & Data
| Product | Reference |
|---------|-----------|
| KV | `references/kv/` |
| D1 | `references/d1/` |
| R2 | `references/r2/` |
| Artifacts | `references/artifacts/` |
| Queues | `references/queues/` |
| Hyperdrive | `references/hyperdrive/` |
| DO Storage | `references/do-storage/` |
| Secrets Store | `references/secrets-store/` |
| Pipelines | `references/pipelines/` |
| R2 Data Catalog | `references/r2-data-catalog/` |
| R2 SQL | `references/r2-sql/` |
### AI & Machine Learning
| Product | Reference |
|---------|-----------|
| Workers AI | `references/workers-ai/` |
| Vectorize | `references/vectorize/` |
| Agents SDK | `references/agents-sdk/` |
| AI Gateway | `references/ai-gateway/` |
| AI Search | `references/ai-search/` |
### Networking & Connectivity
| Product | Reference |
|---------|-----------|
| Tunnel | `references/tunnel/` |
| Spectrum | `references/spectrum/` |
| TURN | `references/turn/` |
| Network Interconnect | `references/network-interconnect/` |
| Argo Smart Routing | `references/argo-smart-routing/` |
| Workers VPC | `references/workers-vpc/` |
### Security
| Product | Reference |
|---------|-----------|
| WAF | `references/waf/` |
| DDoS Protection | `references/ddos/` |
| Bot Management | `references/bot-management/` |
| API Shield | `references/api-shield/` |
| Turnstile | `references/turnstile/` |
### Media & Content
| Product | Reference |
|---------|-----------|
| Images | `references/images/` |
| Stream | `references/stream/` |
| Browser Rendering | `references/browser-rendering/` |
| Zaraz | `references/zaraz/` |
### Real-Time Communication
| Product | Reference |
|---------|-----------|
| RealtimeKit | `references/realtimekit/` |
| Realtime SFU | `references/realtime-sfu/` |
### Developer Tools
| Product | Reference |
|---------|-----------|
| Wrangler | `references/wrangler/` |
| Miniflare | `references/miniflare/` |
| C3 | `references/c3/` |
| Observability | `references/observability/` |
| GraphQL Analytics API | `references/graphql-api/` |
| Analytics Engine | `references/analytics-engine/` |
| Web Analytics | `references/web-analytics/` |
| Sandbox | `references/sandbox/` |
| Workerd | `references/workerd/` |
| Workers Playground | `references/workers-playground/` |
### Infrastructure as Code
| Product | Reference |
|---------|-----------|
| Pulumi | `references/pulumi/` |
| Terraform | `references/terraform/` |
| API | `references/api/` |
### Other Services
| Product | Reference |
|---------|-----------|
| Email Routing | `references/email-routing/` |
| Email Workers | `references/email-workers/` |
| Static Assets | `references/static-assets/` |
| Bindings | `references/bindings/` |
| Cache Reserve | `references/cache-reserve/` |
@@ -0,0 +1,89 @@
# Cloudflare Agents SDK
Cloudflare Agents SDK enables building AI-powered agents on Durable Objects with state, WebSockets, SQL, scheduling, and AI integration.
## Core Value
Build stateful, globally distributed AI agents with persistent memory, real-time connections, scheduled tasks, and async workflows.
## When to Use
- Persistent state + memory required
- Real-time WebSocket connections
- Long-running workflows (minutes/hours)
- Chat interfaces with AI models
- Scheduled/recurring tasks with state
- DB queries with agent state
## What Type of Agent?
| Use Case | Class | Key Features |
|----------|-------|--------------|
| AI chat interface | `AIChatAgent` | Auto-streaming, tools, message history, resumable |
| MCP tool provider | `Agent` + MCP | Expose tools to AI systems |
| Custom logic/routing | `Agent` | Full control, WebSockets, email, SQL |
| Real-time collaboration | `Agent` | WebSocket state, broadcasts |
| Email processing | `Agent` | `onEmail()` handler |
## Quick Start
**AI Chat Agent:**
```typescript
import { AIChatAgent } from "@cloudflare/ai-chat";
import { openai } from "@ai-sdk/openai";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return this.streamText({
model: openai("gpt-4"),
messages: this.messages,
onFinish,
});
}
}
```
**Base Agent:**
```typescript
import { Agent } from "agents";
export class MyAgent extends Agent<Env> {
onStart() {
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`;
}
async onRequest(request: Request) {
return Response.json({ state: this.state });
}
}
```
## Reading Order
| Task | Files to Read |
|------|---------------|
| Quick start | README only |
| Build chat agent | README → api.md (AIChatAgent) → patterns.md |
| Setup project | README → configuration.md |
| Add React frontend | README → api.md (Client Hooks) → patterns.md |
| Build MCP server | api.md (MCP) → patterns.md |
| Background tasks | api.md (Scheduling, Task Queue) → patterns.md |
| Debug issues | gotchas.md |
## Package Entry Points
| Import | Purpose |
|--------|---------|
| `agents` | Server-side Agent classes, lifecycle |
| `agents/react` | `useAgent()` hook for WebSocket connections |
| `agents/ai-react` | `useAgentChat()` hook for AI chat UIs |
## In This Reference
- [configuration.md](./configuration.md) - SDK setup, wrangler config, routing
- [api.md](./api.md) - Agent classes, lifecycle, client hooks
- [patterns.md](./patterns.md) - Common workflows, best practices
- [gotchas.md](./gotchas.md) - Common issues, limits
## See Also
- durable-objects - Agent infrastructure
- d1 - External database integration
- workers-ai - AI model integration
- vectorize - Vector search for RAG patterns
@@ -0,0 +1,190 @@
# API Reference
## Agent Classes
### AIChatAgent
For AI chat with auto-streaming, message history, tools, resumable streaming.
```ts
import { AIChatAgent } from "@cloudflare/ai-chat";
import { openai } from "@ai-sdk/openai";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return this.streamText({
model: openai("gpt-4"),
messages: this.messages, // Auto-managed message history
tools: {
getWeather: {
description: "Get weather",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => `Sunny, 72°F in ${city}`
}
},
onFinish, // Persist response to this.messages
});
}
}
```
### Agent (Base Class)
Full control for custom logic, WebSockets, email, and SQL.
```ts
import { Agent } from "agents";
export class MyAgent extends Agent<Env, State> {
// Lifecycle methods below
}
```
**Type params:** `Agent<Env, State, ConnState>` - Env bindings, agent state, connection state
## Lifecycle Hooks
```ts
onStart() { // Init/restart
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)`;
}
async onRequest(req: Request) { // HTTP
const {pathname} = new URL(req.url);
if (pathname === "/users") return Response.json(this.sql<{id,name}>`SELECT * FROM users`);
return new Response("Not found", {status: 404});
}
async onConnect(conn: Connection<ConnState>, ctx: ConnectionContext) { // WebSocket
conn.accept();
conn.setState({userId: ctx.request.headers.get("X-User-ID")});
conn.send(JSON.stringify({type: "connected", state: this.state}));
}
async onMessage(conn: Connection<ConnState>, msg: WSMessage) { // WS messages
const m = JSON.parse(msg as string);
this.setState({messages: [...this.state.messages, m]});
this.connections.forEach(c => c.send(JSON.stringify(m)));
}
async onEmail(email: AgentEmail) { // Email routing
this.sql`INSERT INTO emails (from_addr,subject,body) VALUES (${email.from},${email.headers.get("subject")},${await email.text()})`;
}
```
## State, SQL, Scheduling
```ts
// State
this.setState({count: 42}); // Auto-syncs
this.setState({...this.state, count: this.state.count + 1});
// SQL (parameterized queries prevent injection)
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)`;
this.sql`INSERT INTO users (id,name) VALUES (${userId},${name})`;
const users = this.sql<{id,name}>`SELECT * FROM users WHERE id = ${userId}`;
// Scheduling
await this.schedule(new Date("2026-12-25"), "sendGreeting", {msg:"Hi"}); // Date
await this.schedule(60, "checkStatus", {}); // Delay (sec)
await this.schedule("0 0 * * *", "dailyCleanup", {}); // Cron
await this.cancelSchedule(scheduleId);
```
## RPC Methods (@callable)
```ts
import { Agent, callable } from "agents";
export class MyAgent extends Agent<Env> {
@callable()
async processTask(input: {text: string}): Promise<{result: string}> {
return { result: await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt: input.text}) };
}
}
// Client: const result = await agent.processTask({ text: "Hello" });
// Must return JSON-serializable values
```
## Connections & AI
```ts
// Connections (type: Agent<Env, State, ConnState>)
this.connections.forEach(c => c.send(JSON.stringify(msg))); // Broadcast
conn.setState({userId:"123"}); conn.close(1000, "Goodbye");
// Workers AI
const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt});
// Manual streaming (prefer AIChatAgent)
const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true});
for await (const chunk of stream) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));
```
**Type-safe state:** `Agent<Env, State, ConnState>` - third param types `conn.state`
## MCP Integration
Model Context Protocol for exposing tools:
```ts
// Register & use MCP server
await this.mcp.registerServer("github", {
url: env.MCP_SERVER_URL,
auth: { type: "oauth", clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET }
});
const tools = await this.mcp.getAITools(["github"]);
return this.streamText({ model: openai("gpt-4"), messages: this.messages, tools, onFinish });
```
## Task Queue
```ts
await this.queue("processVideo", { videoId: "abc123" }); // Add task
const tasks = await this.dequeue(10); // Process up to 10
```
## Context & Cleanup
```ts
const agent = getCurrentAgent<MyAgent>(); // Get current instance
async destroy() { /* cleanup before agent destroyed */ }
```
## AI Integration
```ts
// Workers AI
const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt});
// Manual streaming (prefer AIChatAgent for auto-streaming)
const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true});
for await (const chunk of stream) {
if (chunk.choices[0]?.delta?.content) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));
}
```
## Client Hooks (React)
```ts
// useAgent() - WebSocket connection + RPC
import { useAgent } from "agents/react";
const agent = useAgent({ agent: "MyAgent", name: "user-123" }); // name for idFromName
const result = await agent.processTask({ text: "Hello" }); // Call @callable methods
// agent.readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
// useAgentChat() - AI chat UI
import { useAgentChat } from "@cloudflare/ai-chat/react";
const agent = useAgent({ agent: "ChatAgent" });
const { messages, input, handleInputChange, handleSubmit, isLoading, stop, clearHistory } =
useAgentChat({
agent,
maxSteps: 5, // Max tool iterations
resume: true, // Auto-resume on disconnect
onToolCall: async (toolCall) => {
// Client tools (human-in-the-loop)
if (toolCall.toolName === "confirm") return { ok: window.confirm("Proceed?") };
}
});
// status: "ready" | "submitted" | "streaming" | "error"
```
@@ -0,0 +1,182 @@
# Configuration
## Wrangler Setup
```jsonc
{
"name": "my-agents-app",
"durable_objects": {
"bindings": [
{"name": "MyAgent", "class_name": "MyAgent"}
]
},
"migrations": [
{"tag": "v1", "new_sqlite_classes": ["MyAgent"]}
],
"ai": {
"binding": "AI"
}
}
```
## Environment Bindings
**Type-safe pattern:**
```typescript
interface Env {
AI?: Ai; // Workers AI
MyAgent?: DurableObjectNamespace<MyAgent>;
ChatAgent?: DurableObjectNamespace<ChatAgent>;
DB?: D1Database; // D1 database
KV?: KVNamespace; // KV storage
R2?: R2Bucket; // R2 bucket
OPENAI_API_KEY?: string; // Secrets
GITHUB_CLIENT_ID?: string; // MCP OAuth credentials
GITHUB_CLIENT_SECRET?: string;
QUEUE?: Queue; // Queues
}
```
**Best practice:** Define all DO bindings in Env interface for type safety.
## Deployment
```bash
# Local dev
npx wrangler dev
# Deploy production
npx wrangler deploy
# Set secrets
npx wrangler secret put OPENAI_API_KEY
```
## Agent Routing
**Recommended: Use route helpers**
```typescript
import { routeAgentRequest } from "agents";
export default {
fetch(request: Request, env: Env) {
return routeAgentRequest(request, env);
}
}
```
Helper routes requests to agents automatically based on URL patterns.
**Manual routing (advanced):**
```typescript
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// Named ID (deterministic)
const id = env.MyAgent.idFromName("user-123");
// Random ID (from URL param)
// const id = env.MyAgent.idFromString(url.searchParams.get("id"));
const stub = env.MyAgent.get(id);
return stub.fetch(request);
}
}
```
**Multi-agent setup:**
```typescript
import { routeAgentRequest } from "agents";
export default {
fetch(request: Request, env: Env) {
const url = new URL(request.url);
// Route by path
if (url.pathname.startsWith("/chat")) {
return routeAgentRequest(request, env, "ChatAgent");
}
if (url.pathname.startsWith("/task")) {
return routeAgentRequest(request, env, "TaskAgent");
}
return new Response("Not found", { status: 404 });
}
}
```
## Email Routing
**Code setup:**
```typescript
import { routeAgentEmail } from "agents";
export default {
fetch: (req: Request, env: Env) => routeAgentRequest(req, env),
email: (message: ForwardableEmailMessage, env: Env) => {
return routeAgentEmail(message, env);
}
}
```
**Dashboard setup:**
Configure email routing in Cloudflare dashboard:
```
Destination: Workers with Durable Objects
Worker: my-agents-app
```
Then handle in agent:
```typescript
export class EmailAgent extends Agent<Env> {
async onEmail(email: AgentEmail) {
const text = await email.text();
// Process email
}
}
```
## AI Gateway (Optional)
```typescript
// Enable caching/routing through AI Gateway
const response = await this.env.AI.run(
"@cf/meta/llama-3.1-8b-instruct",
{ prompt },
{
gateway: {
id: "my-gateway-id",
skipCache: false,
cacheTtl: 3600
}
}
);
```
## MCP Configuration (Optional)
For exposing tools via Model Context Protocol:
```typescript
// wrangler.jsonc - Add MCP OAuth secrets
{
"vars": {
"MCP_SERVER_URL": "https://mcp.example.com"
}
}
// Set secrets via CLI
// npx wrangler secret put GITHUB_CLIENT_ID
// npx wrangler secret put GITHUB_CLIENT_SECRET
```
Then register in agent code (see api.md MCP section).
@@ -0,0 +1,158 @@
# Gotchas & Best Practices
## Common Errors
### "setState() not syncing"
**Cause:** Mutating state directly or not calling `setState()` after modifications
**Solution:** Always use `setState()` with immutable updates:
```ts
// ❌ this.state.count++
// ✅ this.setState({...this.state, count: this.state.count + 1})
```
### "Message history grows unbounded (AIChatAgent)"
**Cause:** `this.messages` in `AIChatAgent` accumulates all messages indefinitely
**Solution:** Manually trim old messages periodically:
```ts
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
// Keep only last 50 messages
if (this.messages.length > 50) {
this.messages = this.messages.slice(-50);
}
return this.streamText({ model: openai("gpt-4"), messages: this.messages, onFinish });
}
}
```
### "SQL injection vulnerability"
**Cause:** Direct string interpolation in SQL queries
**Solution:** Use parameterized queries:
```ts
// ❌ this.sql`...WHERE id = '${userId}'`
// ✅ this.sql`...WHERE id = ${userId}`
```
### "WebSocket connection timeout"
**Cause:** Not calling `conn.accept()` in `onConnect`
**Solution:** Always accept connections:
```ts
async onConnect(conn: Connection, ctx: ConnectionContext) { conn.accept(); conn.setState({userId: "123"}); }
```
### "Schedule limit exceeded"
**Cause:** More than 1000 scheduled tasks per agent
**Solution:** Clean up old schedules and limit creation rate:
```ts
async checkSchedules() { if ((await this.getSchedules()).length > 800) console.warn("Near limit!"); }
```
### "AI Gateway unavailable"
**Cause:** AI service timeout or quota exceeded
**Solution:** Add error handling and fallbacks:
```ts
try {
return await this.env.AI.run(model, {prompt});
} catch (e) {
console.error("AI error:", e);
return {error: "Unavailable"};
}
```
### "@callable method returns undefined"
**Cause:** Method doesn't return JSON-serializable value, or has non-serializable types
**Solution:** Ensure return values are plain objects/arrays/primitives:
```ts
// ❌ Returns class instance
@callable()
async getData() { return new Date(); }
// ✅ Returns serializable object
@callable()
async getData() { return { timestamp: Date.now() }; }
```
### "Resumable stream not resuming"
**Cause:** Stream ID must be deterministic for resumption to work
**Solution:** Use AIChatAgent (automatic) or ensure consistent stream IDs:
```ts
// AIChatAgent handles this automatically
export class ChatAgent extends AIChatAgent<Env> {
// Resumption works out of the box
}
```
### "MCP connection loss on hibernation"
**Cause:** MCP server connections don't survive hibernation
**Solution:** Re-register servers in `onStart()` or check connection status:
```ts
onStart() {
// Re-register MCP servers after hibernation
await this.mcp.registerServer("github", { url: env.MCP_URL, auth: {...} });
}
```
### "Agent not found"
**Cause:** Durable Object binding missing or incorrect class name
**Solution:** Verify DO binding in wrangler.jsonc and class name matches
## Rate Limits & Quotas
| Resource/Limit | Value | Notes |
|----------------|-------|-------|
| CPU per request | 30s (std), 300s (max) | Set in wrangler.jsonc |
| Memory per instance | 128MB | Shared with WebSockets |
| Storage per agent | 10GB | SQLite storage |
| Scheduled tasks | 1000 per agent | Monitor with `getSchedules()` |
| WebSocket connections | Unlimited | Within memory limits |
| SQL columns | 100 | Per table |
| SQL row size | 2MB | Key + value |
| WebSocket message | 32MiB | Max size |
| DO requests/sec | ~1000 | Per unique DO instance; rate limit if needed |
| AI Gateway (Workers AI) | Model-specific | Check dashboard for limits |
| MCP requests | Depends on server | Implement retry/backoff |
## Best Practices
### State Management
- Use immutable updates: `setState({...this.state, key: newValue})`
- Trim unbounded arrays (messages, logs) periodically
- Store large data in SQL, not state
### SQL Usage
- Create tables in `onStart()`, not `onRequest()`
- Use parameterized queries: `` sql`WHERE id = ${id}` `` (NOT `` sql`WHERE id = '${id}'` ``)
- Index frequently queried columns
### Scheduling
- Monitor schedule count: `await this.getSchedules()`
- Cancel completed tasks to stay under 1000 limit
- Use cron strings for recurring tasks
### WebSockets
- Always call `conn.accept()` in `onConnect()`
- Handle client disconnects gracefully
- Broadcast to `this.connections` efficiently
### AI Integration
- Use `AIChatAgent` for chat interfaces (auto-streaming, resumption)
- Trim message history to avoid token limits
- Handle AI errors with try/catch and fallbacks
### Production Deployment
- **Rate limiting:** Implement request throttling for high-traffic agents (>1000 req/s)
- **Monitoring:** Log critical errors, track schedule count, monitor storage usage
- **Graceful degradation:** Handle AI service outages with fallbacks
- **Message trimming:** Enforce max history length (e.g., 100 messages) in AIChatAgent
- **MCP reliability:** Re-register servers on hibernation, implement retry logic
@@ -0,0 +1,192 @@
# Patterns & Use Cases
## AI Chat w/Tools
**Server (AIChatAgent):**
```ts
import { AIChatAgent } from "@cloudflare/ai-chat";
import { openai } from "@ai-sdk/openai";
import { tool } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return this.streamText({
model: openai("gpt-4"),
messages: this.messages, // Auto-managed
tools: {
getWeather: tool({
description: "Get current weather",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => `Weather in ${city}: Sunny, 72°F`
}),
searchDocs: tool({
description: "Search documentation",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => JSON.stringify(
this.sql<{title, content}>`SELECT title, content FROM docs WHERE content LIKE ${'%' + query + '%'}`
)
})
},
onFinish,
});
}
}
```
**Client (React):**
```tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
function ChatUI() {
const agent = useAgent({ agent: "ChatAgent" });
const { messages, input, handleInputChange, handleSubmit, isLoading } = useAgentChat({ agent });
return (
<div>
{messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} disabled={isLoading} />
<button disabled={isLoading}>Send</button>
</form>
</div>
);
}
```
## Human-in-the-Loop (Client Tools)
Server defines tool, client executes:
```ts
// Server
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return this.streamText({
model: openai("gpt-4"),
messages: this.messages,
tools: {
confirmAction: tool({
description: "Ask user to confirm",
parameters: z.object({ action: z.string() }),
execute: "client", // Client-side execution
})
},
onFinish,
});
}
}
// Client
const { messages } = useAgentChat({
agent,
onToolCall: async (toolCall) => {
if (toolCall.toolName === "confirmAction") {
return { confirmed: window.confirm(`Confirm: ${toolCall.args.action}?`) };
}
}
});
```
## Task Queue & Scheduled Processing
```ts
export class TaskAgent extends Agent<Env> {
onStart() {
this.schedule("*/5 * * * *", "processQueue", {}); // Every 5 min
this.schedule("0 0 * * *", "dailyCleanup", {}); // Daily
}
async onRequest(req: Request) {
await this.queue("processVideo", { videoId: (await req.json()).videoId });
return Response.json({ queued: true });
}
async processQueue() {
const tasks = await this.dequeue(10);
for (const task of tasks) {
if (task.name === "processVideo") await this.processVideo(task.data.videoId);
}
}
async dailyCleanup() {
this.sql`DELETE FROM logs WHERE created_at < ${Date.now() - 86400000}`;
}
}
```
## Manual WebSocket Chat
Custom protocols (non-AI):
```ts
export class ChatAgent extends Agent<Env> {
async onConnect(conn: Connection, ctx: ConnectionContext) {
conn.accept();
conn.setState({userId: ctx.request.headers.get("X-User-ID") || "anon"});
conn.send(JSON.stringify({type: "history", messages: this.state.messages}));
}
async onMessage(conn: Connection, msg: WSMessage) {
const newMsg = {userId: conn.state.userId, text: JSON.parse(msg as string).text, timestamp: Date.now()};
this.setState({messages: [...this.state.messages, newMsg]});
this.connections.forEach(c => c.send(JSON.stringify(newMsg)));
}
}
```
## Email Processing w/AI
```ts
export class EmailAgent extends Agent<Env> {
async onEmail(email: AgentEmail) {
const [text, from, subject] = [await email.text(), email.from, email.headers.get("subject") || ""];
this.sql`INSERT INTO emails (from_addr, subject, body) VALUES (${from}, ${subject}, ${text})`;
const { text: summary } = await generateText({
model: openai("gpt-4o-mini"), prompt: `Summarize: ${subject}\n\n${text}`
});
this.connections.forEach(c => c.send(JSON.stringify({type: "new_email", from, summary})));
if (summary.includes("urgent")) await this.schedule(0, "sendAutoReply", { to: from });
}
}
```
## Real-time Collaboration
```ts
export class GameAgent extends Agent<Env> {
initialState = { players: [], gameStarted: false };
async onConnect(conn: Connection, ctx: ConnectionContext) {
conn.accept();
const playerId = ctx.request.headers.get("X-Player-ID") || crypto.randomUUID();
conn.setState({ playerId });
const newPlayer = { id: playerId, score: 0 };
this.setState({...this.state, players: [...this.state.players, newPlayer]});
this.connections.forEach(c => c.send(JSON.stringify({type: "player_joined", player: newPlayer})));
}
async onMessage(conn: Connection, msg: WSMessage) {
const m = JSON.parse(msg as string);
if (m.type === "move") {
this.setState({
...this.state,
players: this.state.players.map(p => p.id === conn.state.playerId ? {...p, score: p.score + m.points} : p)
});
this.connections.forEach(c => c.send(JSON.stringify({type: "player_moved", playerId: conn.state.playerId})));
}
if (m.type === "start" && this.state.players.length >= 2) {
this.setState({...this.state, gameStarted: true});
this.connections.forEach(c => c.send(JSON.stringify({type: "game_started"})));
}
}
}
```
@@ -0,0 +1,175 @@
# Cloudflare AI Gateway
Expert guidance for implementing Cloudflare AI Gateway - a universal gateway for AI model providers with analytics, caching, rate limiting, and routing capabilities.
## When to Use This Reference
- Setting up AI Gateway for any AI provider (OpenAI, Anthropic, Workers AI, etc.)
- Implementing caching, rate limiting, or request retry/fallback
- Configuring dynamic routing with A/B testing or model fallbacks
- Managing provider API keys securely with BYOK
- Adding security features (guardrails, DLP)
- Setting up observability with logging and custom metadata
- Debugging AI Gateway requests or optimizing configurations
## Quick Start
**What's your setup?**
- **Using Vercel AI SDK** → Pattern 1 (recommended) - see [sdk-integration.md](./sdk-integration.md)
- **Using OpenAI SDK** → Pattern 2 - see [sdk-integration.md](./sdk-integration.md)
- **Cloudflare Worker + Workers AI** → Pattern 3 - see [sdk-integration.md](./sdk-integration.md)
- **Direct HTTP (any language)** → Pattern 4 - see [configuration.md](./configuration.md)
- **Framework (LangChain, etc.)** → See [sdk-integration.md](./sdk-integration.md)
## Pattern 1: Vercel AI SDK (Recommended)
Most modern pattern using official `ai-gateway-provider` package with automatic fallbacks.
```typescript
import { createAiGateway } from 'ai-gateway-provider';
import { createOpenAI } from '@ai-sdk/openai';
import { generateText } from 'ai';
const gateway = createAiGateway({
accountId: process.env.CF_ACCOUNT_ID,
gateway: process.env.CF_GATEWAY_ID,
});
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY
});
// Single model
const { text } = await generateText({
model: gateway(openai('gpt-4o')),
prompt: 'Hello'
});
// Automatic fallback array
const { text } = await generateText({
model: gateway([
openai('gpt-4o'), // Try first
anthropic('claude-sonnet-4-5'), // Fallback
]),
prompt: 'Hello'
});
```
**Install:** `npm install ai-gateway-provider ai @ai-sdk/openai @ai-sdk/anthropic`
## Pattern 2: OpenAI SDK
Drop-in replacement for OpenAI API with multi-provider support.
```typescript
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/compat`,
defaultHeaders: {
'cf-aig-authorization': `Bearer ${cfToken}` // For authenticated gateways
}
});
// Switch providers by changing model format: {provider}/{model}
const response = await client.chat.completions.create({
model: 'openai/gpt-4o', // or 'anthropic/claude-sonnet-4-5'
messages: [{ role: 'user', content: 'Hello!' }]
});
```
## Pattern 3: Workers AI Binding
For Cloudflare Workers using Workers AI.
```typescript
export default {
async fetch(request, env, ctx) {
const response = await env.AI.run(
'@cf/meta/llama-3-8b-instruct',
{ messages: [{ role: 'user', content: 'Hello!' }] },
{
gateway: {
id: 'my-gateway',
metadata: { userId: '123', team: 'engineering' }
}
}
);
return Response.json(response);
}
};
```
## Headers Quick Reference
| Header | Purpose | Example | Notes |
|--------|---------|---------|-------|
| `cf-aig-authorization` | Gateway auth | `Bearer {token}` | Required for authenticated gateways |
| `cf-aig-metadata` | Tracking | `{"userId":"x"}` | Max 5 entries, flat structure |
| `cf-aig-cache-ttl` | Cache duration | `3600` | Seconds, min 60, max 2592000 (30 days) |
| `cf-aig-skip-cache` | Bypass cache | `true` | - |
| `cf-aig-cache-key` | Custom cache key | `my-key` | Must be unique per response |
| `cf-aig-collect-log` | Skip logging | `false` | Default: true |
| `cf-aig-cache-status` | Cache hit/miss | Response only | `HIT` or `MISS` |
## In This Reference
| File | Purpose |
|------|---------|
| [sdk-integration.md](./sdk-integration.md) | Vercel AI SDK, OpenAI SDK, Workers binding patterns |
| [configuration.md](./configuration.md) | Dashboard setup, wrangler, API tokens |
| [features.md](./features.md) | Caching, rate limits, guardrails, DLP, BYOK, unified billing |
| [dynamic-routing.md](./dynamic-routing.md) | Fallbacks, A/B testing, conditional routing |
| [troubleshooting.md](./troubleshooting.md) | Debugging, errors, observability, gotchas |
## Reading Order
| Task | Files |
|------|-------|
| First-time setup | README + [configuration.md](./configuration.md) |
| SDK integration | README + [sdk-integration.md](./sdk-integration.md) |
| Enable caching | README + [features.md](./features.md) |
| Setup fallbacks | README + [dynamic-routing.md](./dynamic-routing.md) |
| Debug errors | README + [troubleshooting.md](./troubleshooting.md) |
## Architecture
AI Gateway acts as a proxy between your application and AI providers:
```
Your App → AI Gateway → AI Provider (OpenAI, Anthropic, etc.)
↓
Analytics, Caching, Rate Limiting, Logging
```
**Key URL patterns:**
- Unified API (OpenAI-compatible): `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat/chat/completions`
- Provider-specific: `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/{provider}/{endpoint}`
- Dynamic routes: Use route name instead of model: `dynamic/{route-name}`
## Gateway Types
1. **Unauthenticated Gateway**: Open access (not recommended for production)
2. **Authenticated Gateway**: Requires `cf-aig-authorization` header with Cloudflare API token (recommended)
## Provider Authentication Options
1. **Unified Billing**: Use AI Gateway billing to pay for inference (keyless mode - no provider API key needed)
2. **BYOK (Store Keys)**: Store provider API keys in Cloudflare dashboard
3. **Request Headers**: Include provider API key in each request
## Related Skills
- [Workers AI](../workers-ai/README.md) - For `env.AI.run()` details
- [Agents SDK](../agents-sdk/README.md) - For stateful AI patterns
- [Vectorize](../vectorize/README.md) - For RAG patterns with embeddings
## Resources
- [Official Docs](https://developers.cloudflare.com/ai-gateway/)
- [API Reference](https://developers.cloudflare.com/api/resources/ai_gateway/)
- [Provider Guides](https://developers.cloudflare.com/ai-gateway/usage/providers/)
- [Discord Community](https://discord.cloudflare.com)
@@ -0,0 +1,111 @@
# Configuration & Setup
## Creating a Gateway
### Dashboard
AI > AI Gateway > Create Gateway > Configure (auth, caching, rate limiting, logging)
### API
```bash
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
-d '{"id":"my-gateway","cache_ttl":3600,"rate_limiting_interval":60,"rate_limiting_limit":100,"collect_logs":true}'
```
**Naming:** lowercase alphanumeric + hyphens (e.g., `prod-api`, `dev-chat`)
## Wrangler Integration
```toml
[ai]
binding = "AI"
[[ai.gateway]]
id = "my-gateway"
```
```bash
wrangler secret put CF_API_TOKEN
wrangler secret put OPENAI_API_KEY # If not using BYOK
```
## Authentication
### Gateway Auth (protects gateway access)
```typescript
const client = new OpenAI({
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
```
### Provider Auth Options
**1. Unified Billing (keyless)** - pay through Cloudflare, no provider key:
```typescript
const client = new OpenAI({
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
```
Supports: OpenAI, Anthropic, Google AI Studio
**2. BYOK** - store keys in dashboard (Provider Keys > Add), no key in code
**3. Request Headers** - pass provider key per request:
```typescript
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
```
## API Token Permissions
- **Gateway management:** AI Gateway - Read + Edit
- **Gateway access:** AI Gateway - Read (minimum)
## Gateway Management API
```bash
# List
curl https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \
-H "Authorization: Bearer $CF_API_TOKEN"
# Get
curl .../gateways/{gateway_id}
# Update
curl -X PUT .../gateways/{gateway_id} \
-d '{"cache_ttl":7200,"rate_limiting_limit":200}'
# Delete
curl -X DELETE .../gateways/{gateway_id}
```
## Getting IDs
- **Account ID:** Dashboard > Overview > Copy
- **Gateway ID:** AI Gateway > Gateway name column
## Python Example
```python
from openai import OpenAI
import os
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"),
base_url=f"https://gateway.ai.cloudflare.com/v1/{os.environ['CF_ACCOUNT_ID']}/{os.environ['GATEWAY_ID']}/openai",
default_headers={"cf-aig-authorization": f"Bearer {os.environ['CF_API_TOKEN']}"}
)
```
## Best Practices
1. **Always authenticate gateways in production**
2. **Use BYOK or unified billing** - secrets out of code
3. **Environment-specific gateways** - separate dev/staging/prod
4. **Set rate limits** - prevent runaway costs
5. **Enable logging** - track usage, debug issues
@@ -0,0 +1,82 @@
# Dynamic Routing
Configure complex routing in dashboard without code changes. Use route names instead of model names.
## Usage
```typescript
const response = await client.chat.completions.create({
model: 'dynamic/smart-chat', // Route name from dashboard
messages: [{ role: 'user', content: 'Hello!' }]
});
```
## Node Types
| Node | Purpose | Use Case |
|------|---------|----------|
| **Conditional** | Branch on metadata | Paid vs free users, geo routing |
| **Percentage** | A/B split traffic | Model testing, gradual rollouts |
| **Rate Limit** | Enforce quotas | Per-user/team limits |
| **Budget Limit** | Cost quotas | Per-user spending caps |
| **Model** | Call provider | Final destination |
## Metadata
Pass via header (max 5 entries, flat only):
```typescript
headers: {
'cf-aig-metadata': JSON.stringify({
userId: 'user-123',
tier: 'pro',
region: 'us-east'
})
}
```
## Common Patterns
**Multi-model fallback:**
```
Start → GPT-4 → On error: Claude → On error: Llama
```
**Tiered access:**
```
Conditional: tier == 'enterprise' → GPT-4 (no limit)
Conditional: tier == 'pro' → Rate Limit 1000/hr → GPT-4o
Conditional: tier == 'free' → Rate Limit 10/hr → GPT-4o-mini
```
**Gradual rollout:**
```
Percentage: 10% → New model, 90% → Old model
```
**Cost-based fallback:**
```
Budget Limit: $100/day per teamId
< 80%: GPT-4
>= 80%: GPT-4o-mini
>= 100%: Error
```
## Version Management
- Save changes as new version
- Test with `model: 'dynamic/route@v2'`
- Roll back by deploying previous version
## Monitoring
Dashboard → Gateway → Dynamic Routes:
- Request count per path
- Success/error rates
- Latency/cost by path
## Limitations
- Max 5 metadata entries
- Values: string/number/boolean/null only
- No nested objects
- Route names: alphanumeric + hyphens
@@ -0,0 +1,96 @@
# Features & Capabilities
## Caching
Dashboard: Settings → Cache Responses → Enable
```typescript
// Custom TTL (1 hour)
headers: { 'cf-aig-cache-ttl': '3600' }
// Skip cache
headers: { 'cf-aig-skip-cache': 'true' }
// Custom cache key
headers: { 'cf-aig-cache-key': 'greeting-en' }
```
**Limits:** TTL 60s - 30 days. **Does NOT work with streaming.**
## Rate Limiting
Dashboard: Settings → Rate-limiting → Enable
- **Fixed window:** Resets at intervals
- **Sliding window:** Rolling window (more accurate)
- Returns `429` when exceeded
## Guardrails
Dashboard: Settings → Guardrails → Enable
Filter prompts/responses for inappropriate content. Actions: Flag (log) or Block (reject).
## Data Loss Prevention (DLP)
Dashboard: Settings → DLP → Enable
Detect PII (emails, SSNs, credit cards). Actions: Flag, Block, or Redact.
## Billing Modes
| Mode | Description | Setup |
|------|-------------|-------|
| **Unified Billing** | Pay through Cloudflare, no provider keys | Use `cf-aig-authorization` header only |
| **BYOK** | Store provider keys in dashboard | Add keys in Provider Keys section |
| **Pass-through** | Send provider key with each request | Include provider's auth header |
## Zero Data Retention
Dashboard: Settings → Privacy → Zero Data Retention
No prompts/responses stored. Request counts and costs still tracked.
## Logging
Dashboard: Settings → Logs → Enable (up to 10M logs)
Each entry: prompt, response, provider, model, tokens, cost, duration, cache status, metadata.
```typescript
// Skip logging for request
headers: { 'cf-aig-collect-log': 'false' }
```
**Export:** Use Logpush to S3, GCS, Datadog, Splunk, etc.
## Custom Cost Tracking
For models not in Cloudflare's pricing database:
Dashboard: Gateway → Settings → Custom Costs
Or via API: set `model`, `input_cost`, `output_cost`.
## Supported Providers (22+)
| Provider | Unified API | Notes |
|----------|-------------|-------|
| OpenAI | `openai/gpt-4o` | Full support |
| Anthropic | `anthropic/claude-sonnet-4-5` | Full support |
| Google AI | `google-ai-studio/gemini-2.0-flash` | Full support |
| Workers AI | `workersai/@cf/meta/llama-3` | Native |
| Azure OpenAI | `azure-openai/*` | Deployment names |
| AWS Bedrock | Provider endpoint only | `/bedrock/*` |
| Groq | `groq/*` | Fast inference |
| Mistral, Cohere, Perplexity, xAI, DeepSeek, Cerebras | Full support | - |
## Best Practices
1. Enable caching for deterministic prompts
2. Set rate limits to prevent abuse
3. Use guardrails for user-facing AI
4. Enable DLP for sensitive data
5. Use unified billing or BYOK for simpler key management
6. Enable logging for debugging
7. Use zero data retention when privacy required
@@ -0,0 +1,114 @@
# AI Gateway SDK Integration
## Vercel AI SDK (Recommended)
```typescript
import { createAiGateway } from 'ai-gateway-provider';
import { createOpenAI } from '@ai-sdk/openai';
import { generateText } from 'ai';
const gateway = createAiGateway({
accountId: process.env.CF_ACCOUNT_ID,
gateway: process.env.CF_GATEWAY_ID,
apiKey: process.env.CF_API_TOKEN // Optional for auth gateways
});
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Single model
const { text } = await generateText({
model: gateway(openai('gpt-4o')),
prompt: 'Hello'
});
// Automatic fallback array
const { text } = await generateText({
model: gateway([
openai('gpt-4o'),
anthropic('claude-sonnet-4-5'),
openai('gpt-4o-mini')
]),
prompt: 'Complex task'
});
```
### Options
```typescript
model: gateway(openai('gpt-4o'), {
cacheKey: 'my-key',
cacheTtl: 3600,
metadata: { userId: 'u123', team: 'eng' }, // Max 5 entries
retries: { maxAttempts: 3, backoff: 'exponential' }
})
```
## OpenAI SDK
```typescript
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
// Unified API - switch providers via model name
model: 'openai/gpt-4o' // or 'anthropic/claude-sonnet-4-5'
```
## Anthropic SDK
```typescript
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
```
## Workers AI Binding
```toml
# wrangler.toml
[ai]
binding = "AI"
[[ai.gateway]]
id = "my-gateway"
```
```typescript
await env.AI.run('@cf/meta/llama-3-8b-instruct',
{ messages: [...] },
{ gateway: { id: 'my-gateway', metadata: { userId: '123' } } }
);
```
## LangChain / LlamaIndex
```typescript
// Use OpenAI SDK pattern with custom baseURL
new ChatOpenAI({
configuration: {
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`
}
});
```
## HTTP / cURL
```bash
curl https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/chat/completions \
-H "Authorization: Bearer $OPENAI_KEY" \
-H "cf-aig-authorization: Bearer $CF_TOKEN" \
-H "cf-aig-metadata: {\"userId\":\"123\"}" \
-d '{"model":"gpt-4o","messages":[...]}'
```
## Headers Reference
| Header | Purpose |
|--------|---------|
| `cf-aig-authorization` | Gateway auth token |
| `cf-aig-metadata` | JSON object (max 5 keys) |
| `cf-aig-cache-ttl` | Cache TTL in seconds |
| `cf-aig-skip-cache` | `true` to bypass cache |
@@ -0,0 +1,88 @@
# AI Gateway Troubleshooting
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| 401 | Missing `cf-aig-authorization` header | Add header with CF API token |
| 403 | Invalid provider key / BYOK expired | Check provider key in dashboard |
| 429 | Rate limit exceeded | Increase limit or implement backoff |
### 401 Fix
```typescript
const client = new OpenAI({
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${CF_API_TOKEN}` }
});
```
### 429 Retry Pattern
```typescript
async function requestWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try { return await fn(); }
catch (e) {
if (e.status === 429 && i < maxRetries - 1) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
continue;
}
throw e;
}
}
}
```
## Gotchas
| Issue | Reality |
|-------|---------|
| Metadata limits | Max 5 entries, flat only (no nesting) |
| Cache key collision | Use unique keys per expected response |
| BYOK + Unified Billing | Mutually exclusive |
| Rate limit scope | Per-gateway, not per-user (use dynamic routing for per-user) |
| Log delay | 30-60 seconds normal |
| Streaming + caching | **Incompatible** |
| Model name (unified API) | Prefix required: `openai/gpt-4o`, not `gpt-4o` |
## Cache Not Working
**Causes:**
- Different request params (temperature, etc.)
- Streaming enabled
- Caching disabled in settings
**Check:** `response.headers.get('cf-aig-cache-status')` → HIT or MISS
## Logs Not Appearing
1. Check logging enabled: Dashboard → Gateway → Settings
2. Remove `cf-aig-collect-log: false` header
3. Wait 30-60 seconds
4. Check log limit (10M default)
## Debugging
```bash
# Test connectivity
curl -v https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/models \
-H "Authorization: Bearer $OPENAI_KEY" \
-H "cf-aig-authorization: Bearer $CF_TOKEN"
```
```typescript
// Check response headers
console.log('Cache:', response.headers.get('cf-aig-cache-status'));
console.log('Request ID:', response.headers.get('cf-ray'));
```
## Analytics
Dashboard → AI Gateway → Select gateway
**Metrics:** Requests, tokens, latency (p50/p95/p99), cache hit rate, costs
**Log filters:** `status: error`, `provider: openai`, `cost > 0.01`, `duration > 1000`
**Export:** Logpush to S3/GCS/Datadog/Splunk
@@ -0,0 +1,138 @@
# Cloudflare AI Search Reference
Expert guidance for implementing Cloudflare AI Search (formerly AutoRAG), Cloudflare's managed semantic search and RAG service.
## Overview
**AI Search** is a managed RAG (Retrieval-Augmented Generation) pipeline that combines:
- Automatic semantic indexing of your content
- Vector similarity search
- Built-in LLM generation
**Key value propositions:**
- **Zero vector management** - No manual embedding, indexing, or storage
- **Auto-indexing** - Content automatically re-indexed every 6 hours
- **Built-in generation** - Optional AI response generation from retrieved context
- **Multi-source** - Index from R2 buckets or website crawls
**Data source options:**
- **R2 bucket** - Index files from Cloudflare R2 (supports MD, TXT, HTML, PDF, DOC, CSV, JSON)
- **Website** - Crawl and index website content (requires Cloudflare-hosted domain)
**Indexing lifecycle:**
- Automatic 6-hour refresh cycle
- Manual "Force Sync" available (30s rate limit)
- Not designed for real-time updates
## Quick Start
**1. Create AI Search instance in dashboard:**
- Go to Cloudflare Dashboard → AI Search → Create
- Choose data source (R2 or website)
- Configure instance name and settings
**2. Configure Worker:**
```jsonc
// wrangler.jsonc
{
"ai": {
"binding": "AI"
}
}
```
**3. Use in Worker:**
```typescript
export default {
async fetch(request, env) {
const answer = await env.AI.autorag("my-search-instance").aiSearch({
query: "How do I configure caching?",
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
});
return Response.json({ answer: answer.response });
}
};
```
## When to Use AI Search
### AI Search vs Vectorize
| Factor | AI Search | Vectorize |
|--------|-----------|-----------|
| **Management** | Fully managed | Manual embedding + indexing |
| **Use when** | Want zero-ops RAG pipeline | Need custom embeddings/control |
| **Indexing** | Automatic (6hr cycle) | Manual via API |
| **Generation** | Built-in optional | Bring your own LLM |
| **Data sources** | R2 or website | Manual insert |
| **Best for** | Docs, support, enterprise search | Custom ML pipelines, real-time |
### AI Search vs Direct Workers AI
| Factor | AI Search | Workers AI (direct) |
|--------|-----------|---------------------|
| **Context** | Automatic retrieval | Manual context building |
| **Use when** | Need RAG (search + generate) | Simple generation tasks |
| **Indexing** | Built-in | Not applicable |
| **Best for** | Knowledge bases, docs | Simple chat, transformations |
### search() vs aiSearch()
| Method | Returns | Use When |
|--------|---------|----------|
| `search()` | Search results only | Building custom UI, need raw chunks |
| `aiSearch()` | AI response + results | Need ready-to-use answer (chatbot, Q&A) |
### Real-time Updates Consideration
**AI Search is NOT ideal if:**
- Need real-time content updates (<6 hours)
- Content changes multiple times per hour
- Strict freshness requirements
**AI Search IS ideal if:**
- Content relatively stable (docs, policies, knowledge bases)
- 6-hour refresh acceptable
- Prefer zero-ops over real-time
## Platform Limits
| Limit | Value |
|-------|-------|
| Max instances per account | 10 |
| Max files per instance | 100,000 |
| Max file size | 4 MB |
| Index frequency | Every 6 hours |
| Force Sync rate limit | Once per 30 seconds |
| Filter nesting depth | 2 levels |
| Filters per compound | 10 |
| Score threshold range | 0.0 - 1.0 |
## Reading Order
Navigate these references based on your task:
| Task | Read | Est. Time |
|------|------|-----------|
| **Understand AI Search** | README only | 5 min |
| **Implement basic search** | README → api.md | 10 min |
| **Configure data source** | README → configuration.md | 10 min |
| **Production patterns** | patterns.md | 15 min |
| **Debug issues** | gotchas.md | 10 min |
| **Full implementation** | README → api.md → patterns.md | 30 min |
## In This Reference
- **[api.md](api.md)** - API endpoints, methods, TypeScript interfaces
- **[configuration.md](configuration.md)** - Setup, data sources, wrangler config
- **[patterns.md](patterns.md)** - Common patterns, decision guidance, code examples
- **[gotchas.md](gotchas.md)** - Troubleshooting, code-level gotchas, limits
## See Also
- [Cloudflare AI Search Docs](https://developers.cloudflare.com/ai-search/)
- [Workers AI Docs](https://developers.cloudflare.com/workers-ai/)
- [Vectorize Docs](https://developers.cloudflare.com/vectorize/)
@@ -0,0 +1,87 @@
# AI Search API Reference
## Workers Binding
```typescript
const answer = await env.AI.autorag("instance-name").aiSearch(options);
const results = await env.AI.autorag("instance-name").search(options);
const instances = await env.AI.autorag("_").listInstances();
```
## aiSearch() Options
```typescript
interface AiSearchOptions {
query: string; // User query
model: string; // Workers AI model ID
system_prompt?: string; // LLM instructions
rewrite_query?: boolean; // Fix typos (default: false)
max_num_results?: number; // Max chunks (default: 10)
ranking_options?: { score_threshold?: number }; // 0.0-1.0 (default: 0.3)
reranking?: { enabled: boolean; model: string };
stream?: boolean; // Stream response (default: false)
filters?: Filter; // Metadata filters
page?: string; // Pagination token
}
```
## Response
```typescript
interface AiSearchResponse {
search_query: string; // Query used (rewritten if enabled)
response: string; // AI-generated answer
data: SearchResult[]; // Retrieved chunks
has_more: boolean;
next_page?: string;
}
interface SearchResult {
id: string;
score: number;
content: string;
metadata: { filename: string; folder: string; timestamp: number };
}
```
## Filters
```typescript
// Comparison
{ column: "folder", operator: "gte", value: "docs/" }
// Compound
{ operator: "and", filters: [
{ column: "folder", operator: "gte", value: "docs/" },
{ column: "timestamp", operator: "gte", value: 1704067200 }
]}
```
**Operators:** `eq`, `ne`, `gt`, `gte`, `lt`, `lte`
**Built-in metadata:** `filename`, `folder`, `timestamp` (Unix seconds)
## Streaming
```typescript
const stream = await env.AI.autorag("docs").aiSearch({ query, model, stream: true });
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
```
## Error Types
| Error | Cause |
|-------|-------|
| `AutoRAGNotFoundError` | Instance doesn't exist |
| `AutoRAGUnauthorizedError` | Invalid/missing token |
| `AutoRAGValidationError` | Invalid parameters |
## REST API
```bash
curl https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/autorag/rags/{NAME}/ai-search \
-H "Authorization: Bearer {TOKEN}" \
-d '{"query": "...", "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"}'
```
Requires Service API token with "AI Search - Read" permission.
@@ -0,0 +1,88 @@
# AI Search Configuration
## Worker Setup
```jsonc
// wrangler.jsonc
{
"ai": { "binding": "AI" }
}
```
```typescript
interface Env {
AI: Ai;
}
const answer = await env.AI.autorag("my-instance").aiSearch({
query: "How do I configure caching?",
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
});
```
## Data Sources
### R2 Bucket
Dashboard: AI Search → Create Instance → Select R2 bucket
**Supported formats:** `.md`, `.txt`, `.html`, `.pdf`, `.doc`, `.docx`, `.csv`, `.json`
**Auto-indexed metadata:** `filename`, `folder`, `timestamp`
### Website Crawler
Requirements:
- Domain on Cloudflare
- `sitemap.xml` at root
- Bot protection must allow `CloudflareAISearch` user agent
## Path Filtering (R2)
```
docs/**/*.md # All .md in docs/ recursively
**/*.draft.md # Exclude (use in exclude patterns)
```
## Indexing
- **Automatic:** Every 6 hours
- **Force Sync:** Dashboard button (30s rate limit between syncs)
- **Pause:** Settings → Pause Indexing (existing index remains searchable)
## Service API Token
Dashboard: AI Search → Instance → Use AI Search → API → Create Token
Permissions:
- **Read** - search operations
- **Edit** - instance management
Store securely:
```bash
wrangler secret put AI_SEARCH_TOKEN
```
## Multi-Environment
```toml
# wrangler.toml
[env.production.vars]
AI_SEARCH_INSTANCE = "prod-docs"
[env.staging.vars]
AI_SEARCH_INSTANCE = "staging-docs"
```
```typescript
const answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({ query });
```
## Monitoring
```typescript
const instances = await env.AI.autorag("_").listInstances();
console.log(instances.find(i => i.name === "docs"));
```
Dashboard shows: files indexed, status, last index time, storage usage.
@@ -0,0 +1,81 @@
# AI Search Gotchas
## Type Safety
**Timestamp precision:** Use seconds (10-digit), not milliseconds.
```typescript
const nowInSeconds = Math.floor(Date.now() / 1000); // Correct
```
**Folder prefix matching:** Use `gte` for "starts with" on paths.
```typescript
filters: { column: "folder", operator: "gte", value: "docs/api/" } // Matches nested
```
## Filter Limitations
| Limit | Value |
|-------|-------|
| Max nesting depth | 2 levels |
| Filters per compound | 10 |
| `or` operator | Same column, `eq` only |
**OR restriction example:**
```typescript
// ✅ Valid: same column, eq only
{ operator: "or", filters: [
{ column: "folder", operator: "eq", value: "docs/" },
{ column: "folder", operator: "eq", value: "guides/" }
]}
```
## Indexing Issues
| Problem | Cause | Solution |
|---------|-------|----------|
| File not indexed | Unsupported format or >4MB | Check format (.md/.txt/.html/.pdf/.doc/.csv/.json) |
| Index out of sync | 6-hour index cycle | Wait or use "Force Sync" (30s rate limit) |
| Empty results | Index incomplete | Check dashboard for indexing status |
## Auth Errors
| Error | Cause | Fix |
|-------|-------|-----|
| `AutoRAGUnauthorizedError` | Invalid/missing token | Create Service API token with AI Search permissions |
| `AutoRAGNotFoundError` | Wrong instance name | Verify exact name from dashboard |
## Performance
**Slow responses (>3s):**
```typescript
// Add score threshold + limit results
ranking_options: { score_threshold: 0.5 },
max_num_results: 10
```
**Empty results debug:**
1. Remove filters, test basic query
2. Lower `score_threshold` to 0.1
3. Check index is populated
## Limits
| Resource | Limit |
|----------|-------|
| Instances per account | 10 |
| Files per instance | 100,000 |
| Max file size | 4 MB |
| Index frequency | 6 hours |
## Anti-Patterns
**Use env vars for instance names:**
```typescript
const answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({...});
```
**Handle specific error types:**
```typescript
if (error instanceof AutoRAGNotFoundError) { /* 404 */ }
if (error instanceof AutoRAGUnauthorizedError) { /* 401 */ }
```
@@ -0,0 +1,85 @@
# AI Search Patterns
## search() vs aiSearch()
| Use | Method | Returns |
|-----|--------|---------|
| Custom UI, analytics | `search()` | Raw chunks only (~100-300ms) |
| Chatbots, Q&A | `aiSearch()` | AI response + chunks (~500-2000ms) |
## rewrite_query
| Setting | Use When |
|---------|----------|
| `true` | User input (typos, vague queries) |
| `false` | LLM-generated queries (already optimized) |
## Multitenancy (Folder-Based)
```typescript
const answer = await env.AI.autorag("saas-docs").aiSearch({
query: "refund policy",
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
filters: {
column: "folder",
operator: "gte", // "starts with" pattern
value: `tenants/${tenantId}/`
}
});
```
## Streaming
```typescript
const stream = await env.AI.autorag("docs").aiSearch({
query, model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", stream: true
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
```
## Score Threshold
| Threshold | Use |
|-----------|-----|
| 0.3 (default) | Broad recall, exploratory |
| 0.5 | Balanced, production default |
| 0.7 | High precision, critical accuracy |
## System Prompt Template
```typescript
const systemPrompt = `You are a documentation assistant.
- Answer ONLY based on provided context
- If context doesn't contain answer, say "I don't have information"
- Include code examples from context`;
```
## Compound Filters
```typescript
// OR: Multiple folders
filters: {
operator: "or",
filters: [
{ column: "folder", operator: "gte", value: "docs/api/" },
{ column: "folder", operator: "gte", value: "docs/auth/" }
]
}
// AND: Folder + date
filters: {
operator: "and",
filters: [
{ column: "folder", operator: "gte", value: "docs/" },
{ column: "timestamp", operator: "gte", value: oneWeekAgoSeconds }
]
}
```
## Reranking
Enable for high-stakes use cases (adds ~300ms latency):
```typescript
reranking: { enabled: true, model: "@cf/baai/bge-reranker-base" }
```
@@ -0,0 +1,94 @@
# Cloudflare Workers Analytics Engine Reference
Expert guidance for implementing unlimited-cardinality analytics at scale using Cloudflare Workers Analytics Engine.
## What is Analytics Engine?
Time-series analytics database designed for high-cardinality data (millions of unique dimensions). Write data points from Workers, query via SQL API. Use for:
- Custom user-facing analytics dashboards
- Usage-based billing & metering
- Per-customer/per-feature monitoring
- High-frequency instrumentation without performance impact
**Key Capability:** Track metrics with unlimited unique values (e.g., millions of user IDs, API keys) without performance degradation.
## Core Concepts
| Concept | Description | Example |
|---------|-------------|---------|
| **Dataset** | Logical table for related metrics | `api_requests`, `user_events` |
| **Data Point** | Single measurement with timestamp | One API request's metrics |
| **Blobs** | String dimensions (max 20) | endpoint, method, status, user_id |
| **Doubles** | Numeric values (max 20) | latency_ms, request_count, bytes |
| **Indexes** | Filtered blobs for efficient queries | customer_id, api_key |
## Reading Order
| Task | Start Here | Then Read |
|------|------------|-----------|
| **First-time setup** | [configuration.md](configuration.md) → [api.md](api.md) → [patterns.md](patterns.md) | |
| **Writing data** | [api.md](api.md) → [gotchas.md](gotchas.md) (sampling) | |
| **Querying data** | [api.md](api.md) (SQL API) → [patterns.md](patterns.md) (examples) | |
| **Debugging** | [gotchas.md](gotchas.md) → [api.md](api.md) (limits) | |
| **Optimization** | [patterns.md](patterns.md) (anti-patterns) → [gotchas.md](gotchas.md) | |
## When to Use Analytics Engine
```
Need to track metrics? → Yes
↓
Millions of unique dimension values? → Yes
↓
Need real-time queries? → Yes
↓
Use Analytics Engine ✓
Alternative scenarios:
- Low cardinality (<10k unique values) → Workers Analytics (free tier)
- Complex joins/relations → D1 Database
- Logs/debugging → Tail Workers (logpush)
- External tools → Send to external analytics (Datadog, etc.)
```
## Quick Start
1. Add binding to `wrangler.jsonc`:
```jsonc
{
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "my_events" }
]
}
```
2. Write data points (fire-and-forget, no await):
```typescript
env.ANALYTICS.writeDataPoint({
blobs: ["/api/users", "GET", "200"],
doubles: [145.2, 1], // latency_ms, count
indexes: [customerId]
});
```
3. Query via SQL API (HTTP):
```sql
SELECT blob1, SUM(double2) AS total_requests
FROM my_events
WHERE index1 = 'customer_123'
AND timestamp >= NOW() - INTERVAL '7' DAY
GROUP BY blob1
ORDER BY total_requests DESC
```
## In This Reference
- **[configuration.md](configuration.md)** - Setup, bindings, TypeScript types, limits
- **[api.md](api.md)** - `writeDataPoint()`, SQL API, query syntax
- **[patterns.md](patterns.md)** - Use cases, examples, anti-patterns
- **[gotchas.md](gotchas.md)** - Sampling, index selection, troubleshooting
## See Also
- [Cloudflare Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/)
- [GraphQL Analytics API Reference](../graphql-api/) - Query built-in Cloudflare analytics (HTTP, Workers, DNS, Firewall, etc.)
- [Observability Reference](../observability/) - Workers Logs, Traces, and real-time debugging
@@ -0,0 +1,112 @@
# Analytics Engine API Reference
## Writing Data
### `writeDataPoint()`
Fire-and-forget (returns `void`, not Promise). Writes happen asynchronously.
```typescript
interface AnalyticsEngineDataPoint {
blobs?: string[]; // Up to 20 strings (dimensions), 16KB each
doubles?: number[]; // Up to 20 numbers (metrics)
indexes?: string[]; // 1 indexed string for high-cardinality filtering
}
env.ANALYTICS.writeDataPoint({
blobs: ["/api/users", "GET", "200"],
doubles: [145.2, 1], // latency_ms, count
indexes: ["customer_abc123"]
});
```
**Behaviors:** No await needed, no error thrown (check tail logs), auto-sampled at high volumes, auto-timestamped.
**Blob vs Index:** Blob for GROUP BY (<100k unique), Index for filter-only (millions unique).
### Full Example
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const start = Date.now();
const url = new URL(request.url);
try {
const response = await handleRequest(request);
env.ANALYTICS.writeDataPoint({
blobs: [url.pathname, request.method, response.status.toString()],
doubles: [Date.now() - start, 1],
indexes: [request.headers.get("x-api-key") || "anonymous"]
});
return response;
} catch (error) {
env.ANALYTICS.writeDataPoint({
blobs: [url.pathname, request.method, "500"],
doubles: [Date.now() - start, 1, 0],
});
throw error;
}
}
};
```
## SQL API (External Only)
```bash
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql \
-H "Authorization: Bearer $TOKEN" \
-d "SELECT blob1 AS endpoint, COUNT(*) AS requests FROM dataset WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY blob1"
```
### Column References
```sql
-- blob1..blob20, double1..double20, index1, timestamp
SELECT blob1 AS endpoint, SUM(double1) AS latency, COUNT(*) AS requests
FROM my_dataset
WHERE index1 = 'customer_123' AND timestamp >= NOW() - INTERVAL '7' DAY
GROUP BY blob1
HAVING COUNT(*) > 100
ORDER BY requests DESC LIMIT 100
```
**Aggregations:** `SUM()`, `AVG()`, `COUNT()`, `MIN()`, `MAX()`, `quantile(0.95)()`
**Time ranges:** `NOW() - INTERVAL '1' HOUR`, `BETWEEN '2026-01-01' AND '2026-01-31'`
### Query Examples
```sql
-- Top endpoints
SELECT blob1, COUNT(*) AS requests, AVG(double1) AS avg_latency
FROM api_requests WHERE timestamp >= NOW() - INTERVAL '24' HOUR
GROUP BY blob1 ORDER BY requests DESC LIMIT 20
-- Error rate
SELECT blob1, COUNT(*) AS total,
SUM(CASE WHEN blob3 LIKE '5%' THEN 1 ELSE 0 END) AS errors
FROM api_requests WHERE timestamp >= NOW() - INTERVAL '1' HOUR
GROUP BY blob1 HAVING total > 50
-- P95 latency
SELECT blob1, quantile(0.95)(double1) AS p95
FROM api_requests GROUP BY blob1
```
## Response Format
```json
{"data": [{"endpoint": "/api/users", "requests": 1523}], "rows": 2}
```
## Limits
| Resource | Limit |
|----------|-------|
| Blobs/Doubles per point | 20 each |
| Indexes per point | 1 |
| Blob/Index size | 16KB |
| Data retention | 90 days |
| Query timeout | 30s |
**Critical:** High write volumes (>1M/min) trigger automatic sampling.
@@ -0,0 +1,112 @@
# Analytics Engine Configuration
## Setup
1. Add binding to `wrangler.jsonc`
2. Deploy Worker
3. Dataset created automatically on first write
4. Query via SQL API
## wrangler.jsonc
```jsonc
{
"name": "my-worker",
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "my_events" }
]
}
```
Multiple datasets for separate concerns:
```jsonc
{
"analytics_engine_datasets": [
{ "binding": "API_ANALYTICS", "dataset": "api_requests" },
{ "binding": "USER_EVENTS", "dataset": "user_activity" }
]
}
```
## TypeScript
```typescript
interface Env {
ANALYTICS: AnalyticsEngineDataset;
}
export default {
async fetch(request: Request, env: Env) {
// No await - returns void, fire-and-forget
env.ANALYTICS.writeDataPoint({
blobs: [pathname, method, status], // String dimensions (max 20)
doubles: [latency, 1], // Numeric metrics (max 20)
indexes: [apiKey] // High-cardinality filter (max 1)
});
return response;
}
};
```
## Data Point Limits
| Field | Limit | SQL Access |
|-------|-------|------------|
| blobs | 20 strings, 16KB each | `blob1`...`blob20` |
| doubles | 20 numbers | `double1`...`double20` |
| indexes | 1 string, 16KB | `index1` |
## Write Behavior
| Scenario | Behavior |
|----------|----------|
| <1M writes/min | All accepted |
| >1M writes/min | Automatic sampling |
| Invalid data | Silent failure (check tail logs) |
**Mitigate sampling:** Pre-aggregate, use multiple datasets, write only critical metrics.
## Query Limits
| Resource | Limit |
|----------|-------|
| Query timeout | 30 seconds |
| Data retention | 90 days (default) |
| Result size | ~10MB |
## Cost
**Free tier:** 10M writes/month, 1M reads/month
**Paid:** $0.05 per 1M writes, $1.00 per 1M reads
## Environment-Specific
```jsonc
{
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "prod_events" }
],
"env": {
"staging": {
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "staging_events" }
]
}
}
}
```
## Monitoring
```bash
npx wrangler tail # Check for sampling/write errors
```
```sql
-- Check write activity
SELECT DATE_TRUNC('hour', timestamp) AS hour, COUNT(*) AS writes
FROM my_dataset
WHERE timestamp >= NOW() - INTERVAL '24' HOUR
GROUP BY hour
```
@@ -0,0 +1,85 @@
# Analytics Engine Gotchas
## Critical Issues
### Sampling at High Volumes
**Problem:** Queries return fewer points than written at >1M writes/min.
**Solution:**
```typescript
// Pre-aggregate before writing
let buffer = { count: 0, total: 0 };
buffer.count++; buffer.total += value;
// Write once per second instead of per request
if (Date.now() % 1000 === 0) {
env.ANALYTICS.writeDataPoint({ doubles: [buffer.count, buffer.total] });
}
```
**Detection:** `npx wrangler tail` → look for "sampling enabled"
### writeDataPoint Returns void
```typescript
// ❌ Pointless await
await env.ANALYTICS.writeDataPoint({...});
// ✅ Fire-and-forget
env.ANALYTICS.writeDataPoint({...});
```
Writes can fail silently. Check tail logs.
### Index vs Blob
| Cardinality | Use | Example |
|-------------|-----|---------|
| Millions | **Index** | user_id, api_key |
| Hundreds | **Blob** | endpoint, status_code, country |
```typescript
// ✅ Correct
{ blobs: [method, path, status], indexes: [userId] }
```
### Can't Query from Workers
Query API requires HTTP auth. Use external service or cache in KV/D1.
### No Custom Timestamps
Auto-generated at write time. Store original in blob if needed.
## Common Errors
| Error | Fix |
|-------|-----|
| Binding not found | Check wrangler.jsonc, redeploy |
| No data in query | Wait 30s; check dataset name; check time range |
| Query timeout | Add time filter; use index for filtering |
## Limits
| Resource | Limit |
|----------|-------|
| Blobs per point | 20 |
| Doubles per point | 20 |
| Indexes per point | 1 |
| Blob/Index size | 16KB |
| Write rate (no sampling) | ~1M/min |
| Retention | 90 days |
| Query timeout | 30s |
## Best Practices
✅ Pre-aggregate at high volumes
✅ Use index for high-cardinality (millions)
✅ Always include time filter in queries
✅ Design schema before coding
❌ Don't await writeDataPoint
❌ Don't use index for low-cardinality
❌ Don't query without time range
❌ Don't assume all writes succeed
@@ -0,0 +1,83 @@
# Analytics Engine Patterns
## Use Cases
| Use Case | Key Metrics | Index On |
|----------|-------------|----------|
| API Metering | requests, bytes, compute_units | api_key |
| Feature Usage | feature, action, duration | user_id |
| Error Tracking | error_type, endpoint, count | customer_id |
| Performance | latency_ms, cache_status | endpoint |
| A/B Testing | variant, conversions | user_id |
## API Metering (Billing)
```typescript
env.ANALYTICS.writeDataPoint({
blobs: [pathname, method, status, tier],
doubles: [1, computeUnits, bytes, latencyMs],
indexes: [apiKey]
});
// Query: Monthly usage by customer
// SELECT index1 AS api_key, SUM(double2) AS compute_units
// FROM usage WHERE timestamp >= DATE_TRUNC('month', NOW()) GROUP BY index1
```
## Error Tracking
```typescript
env.ANALYTICS.writeDataPoint({
blobs: [endpoint, method, errorName, errorMessage.slice(0, 1000)],
doubles: [1, timeToErrorMs],
indexes: [customerId]
});
```
## Performance Monitoring
```typescript
env.ANALYTICS.writeDataPoint({
blobs: [pathname, method, cacheStatus, status],
doubles: [latencyMs, 1],
indexes: [userId]
});
// Query: P95 latency by endpoint
// SELECT blob1, quantile(0.95)(double1) AS p95_ms FROM perf GROUP BY blob1
```
## Anti-Patterns
| ❌ Wrong | ✅ Correct |
|----------|-----------|
| `await writeDataPoint()` | `writeDataPoint()` (fire-and-forget) |
| `indexes: [method]` (low cardinality) | `blobs: [method]`, `indexes: [userId]` |
| `blobs: [JSON.stringify(obj)]` | Store ID in blob, full object in D1/KV |
| Write every request at 10M/min | Pre-aggregate per second |
| Query from Worker | Query from external service/API |
## Best Practices
1. **Design schema upfront** - Document blob/double/index assignments
2. **Always include count metric** - `doubles: [latency, 1]` for AVG calculations
3. **Use enums for blobs** - Consistent values like `Status.SUCCESS`
4. **Handle sampling** - Use ratios (avg_latency = SUM(latency)/SUM(count))
5. **Test queries early** - Validate schema before heavy writes
## Schema Template
```typescript
/**
* Dataset: my_metrics
*
* Blobs:
* blob1: endpoint, blob2: method, blob3: status
*
* Doubles:
* double1: latency_ms, double2: count (always 1)
*
* Indexes:
* index1: customer_id (high cardinality)
*/
```
@@ -0,0 +1,44 @@
# Cloudflare API Shield Reference
Expert guidance for API Shield - comprehensive API security suite for discovery, protection, and monitoring.
## Reading Order
| Task | Files to Read |
|------|---------------|
| Initial setup | README → configuration.md |
| Implement JWT validation | configuration.md → api.md |
| Add schema validation | configuration.md → patterns.md |
| Detect API attacks | patterns.md → api.md |
| Debug issues | gotchas.md |
## Feature Selection
What protection do you need?
```
├─ Validate request/response structure → Schema Validation 2.0 (configuration.md)
├─ Verify auth tokens → JWT Validation (configuration.md)
├─ Client certificates → mTLS (configuration.md)
├─ Detect BOLA attacks → BOLA Detection (patterns.md)
├─ Track auth coverage → Auth Posture (patterns.md)
├─ Stop volumetric abuse → Abuse Detection (patterns.md)
└─ Discover shadow APIs → API Discovery (api.md)
```
## In This Reference
- **[configuration.md](configuration.md)** - Setup, session identifiers, rules, token/mTLS configs
- **[api.md](api.md)** - Endpoint management, discovery, validation APIs, GraphQL operations
- **[patterns.md](patterns.md)** - Common patterns, progressive rollout, OWASP mappings, workflows
- **[gotchas.md](gotchas.md)** - Troubleshooting, false positives, performance, best practices
## Quick Start
API Shield: Enterprise-grade API security (Discovery, Schema Validation 2.0, JWT, mTLS, BOLA Detection, Auth Posture). Available as Enterprise add-on with preview access.
## See Also
- [API Shield Docs](https://developers.cloudflare.com/api-shield/)
- [API Reference](https://developers.cloudflare.com/api/resources/api_gateway/)
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
@@ -0,0 +1,141 @@
# API Reference
Base: `/zones/{zone_id}/api_gateway`
## Endpoints
```bash
GET /operations # List
GET /operations/{op_id} # Get single
POST /operations/item # Create: {endpoint,host,method}
POST /operations # Bulk: {operations:[{endpoint,host,method}]}
DELETE /operations/{op_id} # Delete
DELETE /operations # Bulk delete: {operation_ids:[...]}
```
## Discovery
```bash
GET /discovery/operations # List discovered
PATCH /discovery/operations/{op_id} # Update: {state:"saved"|"ignored"}
PATCH /discovery/operations # Bulk: {operation_ids:{id:{state}}}
GET /discovery # OpenAPI export
```
## Config
```bash
GET /configuration # Get session ID config
PUT /configuration # Update: {auth_id_characteristics:[{name,type:"header"|"cookie"}]}
```
## Token Validation
```bash
GET /token_validation # List
POST /token_validation # Create: {name,location:{header:"..."},jwks:"..."}
POST /jwt_validation_rules # Rule: {name,hostname,token_validation_id,action:"block"}
```
## Workers Integration
### Access JWT Claims
```js
export default {
async fetch(req, env) {
// Access validated JWT payload
const jwt = req.cf?.jwt?.payload?.[env.JWT_CONFIG_ID]?.[0];
if (jwt) {
const userId = jwt.sub;
const role = jwt.role;
}
}
}
```
### Access mTLS Info
```js
export default {
async fetch(req, env) {
const tls = req.cf?.tlsClientAuth;
if (tls?.certVerified === 'SUCCESS') {
const fingerprint = tls.certFingerprintSHA256;
// Authenticated client
}
}
}
```
### Dynamic JWKS Update
```js
export default {
async scheduled(event, env) {
const jwks = await (await fetch('https://auth.example.com/.well-known/jwks.json')).json();
await fetch(`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/api_gateway/token_validation/${env.CONFIG_ID}`, {
method: 'PATCH',
headers: {'Authorization': `Bearer ${env.CF_API_TOKEN}`, 'Content-Type': 'application/json'},
body: JSON.stringify({jwks: JSON.stringify(jwks)})
});
}
}
```
## Firewall Fields
### Core Fields
```js
cf.api_gateway.auth_id_present // Session ID present
cf.api_gateway.request_violates_schema // Schema violation
cf.api_gateway.fallthrough_triggered // No endpoint match
cf.tls_client_auth.cert_verified // mTLS cert valid
cf.tls_client_auth.cert_fingerprint_sha256
```
### JWT Validation (2026)
```js
// Modern validation syntax
is_jwt_valid(http.request.jwt.payload["{config_id}"][0])
// Legacy (still supported)
cf.api_gateway.jwt_claims_valid
// Extract claims
lookup_json_string(http.request.jwt.payload["{config_id}"][0], "claim_name")
```
### Risk Labels (2026)
```js
// BOLA detection
cf.api_gateway.cf-risk-bola-enumeration // Sequential resource access detected
cf.api_gateway.cf-risk-bola-pollution // Parameter pollution detected
// Authentication posture
cf.api_gateway.cf-risk-missing-auth // Endpoint lacks authentication
cf.api_gateway.cf-risk-mixed-auth // Inconsistent auth patterns
```
## BOLA Detection
```bash
GET /user_schemas/{schema_id}/bola # Get BOLA config
PATCH /user_schemas/{schema_id}/bola # Update: {enabled:true}
```
## Auth Posture
```bash
GET /discovery/authentication_posture # List unprotected endpoints
```
## GraphQL Protection
```bash
GET /settings/graphql_protection # Get limits
PUT /settings/graphql_protection # Set: {max_depth,max_size}
```
## See Also
- [configuration.md](configuration.md) - Setup guides for all features
- [patterns.md](patterns.md) - Firewall rules and common patterns
- [API Gateway API Docs](https://developers.cloudflare.com/api/resources/api_gateway/)
@@ -0,0 +1,192 @@
# Configuration
## Schema Validation 2.0 Setup
> ⚠️ **Classic Schema Validation deprecated.** Use Schema Validation 2.0.
**Upload schema (Dashboard):**
```
Security > API Shield > Schema Validation > Add validation
- Upload .yml/.yaml/.json (OpenAPI v3.0)
- Endpoints auto-added to Endpoint Management
- Action: Log | Block | None
- Body inspection: JSON payloads
```
**Change validation action:**
```
Security > API Shield > Settings > Schema Validation
Per-endpoint: Filter → ellipses → Change action
Default action: Set global mitigation action
```
**Migration from Classic:**
```
1. Export existing schema (if available)
2. Delete all Classic schema validation rules
3. Wait 5 min for cache clear
4. Re-upload via Schema Validation 2.0 interface
5. Verify in Security > Events
```
**Fallthrough rule** (catch-all unknown endpoints):
```
Security > API Shield > Settings > Fallthrough > Use Template
- Select hostnames
- Create rule with cf.api_gateway.fallthrough_triggered
- Action: Log (discover) or Block (strict)
```
**Body inspection:** Supports `application/json`, `*/*`, `application/*`. Disable origin MIME sniffing to prevent bypasses.
## JWT Validation
**Setup token config:**
```
Security > API Shield > Settings > JWT Settings > Add configuration
- Name: "Auth0 JWT Config"
- Location: Header/Cookie + name (e.g., "Authorization")
- JWKS: Paste public keys from IdP
```
**Create validation rule:**
```
Security > API Shield > API Rules > Add rule
- Hostname: api.example.com
- Deselect endpoints to ignore
- Token config: Select config
- Enforce presence: Ignore or Mark as non-compliant
- Action: Log/Block/Challenge
```
**Rate limit by JWT claim:**
```wirefilter
lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub")
```
**Special cases:**
- Two JWTs, different IdPs: Create 2 configs, select both, "Validate all"
- IdP migration: 2 configs + 2 rules, adjust actions per state
- Bearer prefix: API Shield handles with/without
- Nested claims: Dot notation `user.email`
## Mutual TLS (mTLS)
**Setup:**
```
SSL/TLS > Client Certificates > Create Certificate
- Generate CF-managed CA (all plans)
- Upload custom CA (Enterprise, max 5)
```
**Configure mTLS rule:**
```
Security > API Shield > mTLS
- Select hostname(s)
- Choose certificate(s)
- Action: Block/Log/Challenge
```
**Test:**
```bash
openssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365
curl https://api.example.com/endpoint --cert client-cert.pem --key client-key.pem
```
## Session Identifiers
Critical for BOLA Detection, Sequence Mitigation, and analytics. Configure header/cookie that uniquely IDs API users.
**Examples:** JWT sub claim, session token, API key, custom user ID header
**Configure:**
```
Security > API Shield > Settings > Session Identifiers
- Type: Header/Cookie
- Name: "X-User-ID" or "Authorization"
```
## BOLA Detection
Detects Broken Object Level Authorization attacks (enumeration + parameter pollution).
**Enable:**
```
Security > API Shield > Schema Validation > [Select Schema] > BOLA Detection
- Enable detection
- Threshold: Sensitivity level (Low/Medium/High)
- Action: Log or Block
```
**Requirements:**
- Schema Validation 2.0 enabled
- Session identifiers configured
- Minimum traffic: 1000+ requests/day per endpoint
## Authentication Posture
Identifies unprotected or inconsistently protected endpoints.
**View report:**
```
Security > API Shield > Authentication Posture
- Shows endpoints lacking JWT/mTLS
- Highlights mixed authentication patterns
```
**Remediate:**
1. Review flagged endpoints
2. Add JWT validation rules
3. Configure mTLS for sensitive endpoints
4. Monitor posture score
## Volumetric Abuse + GraphQL
**Volumetric Abuse Detection:**
`Security > API Shield > Settings > Volumetric Abuse Detection`
- Enable per-endpoint monitoring, set thresholds, action: Log | Challenge | Block
**GraphQL Protection:**
`Security > API Shield > Settings > GraphQL Protection`
- Max query depth: 10, max size: 100KB, block introspection (production)
## Terraform
```hcl
# Session identifier
resource "cloudflare_api_shield" "main" {
zone_id = var.zone_id
auth_id_characteristics {
type = "header"
name = "Authorization"
}
}
# Add endpoint
resource "cloudflare_api_shield_operation" "users_get" {
zone_id = var.zone_id
method = "GET"
host = "api.example.com"
endpoint = "/api/users/{id}"
}
# JWT validation rule
resource "cloudflare_ruleset" "jwt_validation" {
zone_id = var.zone_id
name = "API JWT Validation"
kind = "zone"
phase = "http_request_firewall_custom"
rules {
action = "block"
expression = "(http.host eq \"api.example.com\" and not is_jwt_valid(http.request.jwt.payload[\"{config_id}\"][0]))"
description = "Block invalid JWTs"
}
}
```
## See Also
- [api.md](api.md) - API endpoints and Workers integration
- [patterns.md](patterns.md) - Firewall rules and deployment patterns
- [gotchas.md](gotchas.md) - Troubleshooting and limits
@@ -0,0 +1,125 @@
# Gotchas & Troubleshooting
## Common Errors
### "Schema Validation 2.0 not working after migration"
**Cause:** Classic rules still active, conflicting with new system
**Solution:**
1. Delete ALL Classic schema validation rules
2. Clear Cloudflare cache (wait 5 min)
3. Re-upload schema via new Schema Validation 2.0 interface
4. Verify in Security > Events
5. Check action is set (Log/Block)
### "Schema validation blocking valid requests"
**Cause:** Schema too restrictive, missing fields, or incorrect types
**Solution:**
1. Check Firewall Events for violation details
2. Review schema in Settings
3. Test schema in Swagger Editor
4. Use Log mode to validate before blocking
5. Update schema with correct specifications
6. Ensure Schema Validation 2.0 (not Classic)
### "JWT validation failing"
**Cause:** JWKS mismatch with IdP, expired token, wrong header/cookie name, or clock skew
**Solution:**
1. Verify JWKS matches IdP configuration
2. Check token `exp` claim is valid
3. Confirm header/cookie name matches config
4. Test token at jwt.io
5. Account for clock skew (±5 min tolerance)
6. Use modern syntax: `is_jwt_valid(http.request.jwt.payload["{config_id}"][0])`
### "BOLA detection false positives"
**Cause:** Legitimate sequential access patterns, bulk operations, or sensitivity too high
**Solution:**
1. Review BOLA events in Security > Events
2. Lower sensitivity threshold (High → Medium → Low)
3. Exclude legitimate bulk operations from detection
4. Ensure session identifiers uniquely identify users
5. Verify minimum traffic requirements met (1000+ req/day)
### "Risk labels not appearing in firewall rules"
**Cause:** Feature not enabled, insufficient traffic, or missing session identifiers
**Solution:**
1. Verify Schema Validation 2.0 enabled
2. Enable BOLA Detection in schema settings
3. Configure session identifiers (required for BOLA)
4. Wait 24-48h for ML model training
5. Check minimum traffic thresholds met
### "Endpoint discovery not finding APIs"
**Cause:** Insufficient traffic (<500 reqs/10d), non-2xx responses, Worker direct requests, or incorrect session ID config
**Solution:** Ensure 500+ requests in 10 days, 2xx responses from edge (not Workers direct), configure session IDs correctly. ML updates daily.
### "Sequence detection false positives"
**Cause:** Lookback window issues, non-unique session IDs, or model sensitivity
**Solution:**
1. Review lookback settings (10 reqs to managed endpoints, 10min window)
2. Ensure session ID uniqueness per user (not shared tokens)
3. Adjust positive/negative model balance
4. Exclude legitimate workflows from detection
### "GraphQL protection blocking valid queries"
**Cause:** Query depth/size limits too restrictive, complex but legitimate queries
**Solution:**
1. Review blocked query patterns in Security > Events
2. Increase max_depth (default: 10) if needed
3. Increase max_size (default: 100KB) for complex queries
4. Whitelist specific query signatures
5. Use Log mode to tune before blocking
### "Token invalid"
**Cause:** Configuration error, JWKS mismatch, or expired token
**Solution:** Verify config matches IdP, update JWKS, check token expiration
### "Schema violation"
**Cause:** Missing required fields, wrong data types, or spec mismatch
**Solution:** Review schema against actual requests, ensure all required fields present, validate types match spec
### "Fallthrough"
**Cause:** Unknown endpoint or pattern mismatch
**Solution:** Update schema with all endpoints, check path pattern matching
### "mTLS failed"
**Cause:** Certificate untrusted/expired or wrong CA
**Solution:** Verify cert chain, check expiration, confirm correct CA uploaded
## Limits (2026)
| Resource/Limit | Value | Notes |
|----------------|-------|-------|
| OpenAPI version | v3.0.x only | No external refs, must be valid |
| Schema operations | 10K (Enterprise) | Contact for higher limits |
| JWT validation sources | Headers/cookies only | No query params/body |
| Endpoint discovery | 500+ reqs/10d | Minimum for ML model |
| Path normalization | Automatic | `/profile/238` → `/profile/{var1}` |
| Schema parameters | No `content` field | No object param validation |
| BOLA detection | 1000+ reqs/day/endpoint | Per-endpoint minimum |
| Session ID uniqueness | Required | BOLA/Sequence need unique IDs |
| GraphQL max depth | 1-50 | Default: 10 |
| GraphQL max size | 1KB-1MB | Default: 100KB |
| JWT claim nesting | 10 levels max | Use dot notation |
| mTLS CA certificates | 5 custom max | CF-managed unlimited |
| Schema upload size | 5MB max | Compressed OpenAPI spec |
| Volumetric abuse baseline | 7 days training | Initial ML period |
| Auth Posture refresh | Daily | Updated nightly |
## See Also
- [configuration.md](configuration.md) - Setup guides to avoid common issues
- [patterns.md](patterns.md) - Best practices and progressive rollout
- [API Shield Docs](https://developers.cloudflare.com/api-shield/)
@@ -0,0 +1,180 @@
# Patterns & Use Cases
## Protect API with Schema + JWT
```bash
# 1. Upload OpenAPI schema
POST /zones/{zone_id}/api_gateway/user_schemas
# 2. Configure JWT validation
POST /zones/{zone_id}/api_gateway/token_validation
{
"name": "Auth0",
"location": {"header": "Authorization"},
"jwks": "{...}"
}
# 3. Create JWT rule
POST /zones/{zone_id}/api_gateway/jwt_validation_rules
# 4. Set schema validation action
PUT /zones/{zone_id}/api_gateway/settings/schema_validation
{"validation_default_mitigation_action": "block"}
```
## Progressive Rollout
```
1. Log mode: Observe false positives
- Schema: Action = Log
- JWT: Action = Log
2. Block subset: Protect critical endpoints
- Change specific endpoint actions to Block
- Monitor firewall events
3. Full enforcement: Block all violations
- Change default action to Block
- Handle fallthrough with custom rule
```
## BOLA Detection
### Enumeration Detection
Detects sequential resource access (e.g., `/users/1`, `/users/2`, `/users/3`).
```javascript
// Block BOLA enumeration attempts
(cf.api_gateway.cf-risk-bola-enumeration and http.host eq "api.example.com")
// Action: Block or Challenge
```
### Parameter Pollution
Detects duplicate/excessive parameters in requests.
```javascript
// Block parameter pollution
(cf.api_gateway.cf-risk-bola-pollution and http.host eq "api.example.com")
// Action: Block
```
### Combined BOLA Protection
```javascript
// Comprehensive BOLA rule
(cf.api_gateway.cf-risk-bola-enumeration or cf.api_gateway.cf-risk-bola-pollution)
and http.host eq "api.example.com"
// Action: Block
```
## Authentication Posture
### Detect Missing Auth
```javascript
// Log endpoints lacking authentication
(cf.api_gateway.cf-risk-missing-auth and http.host eq "api.example.com")
// Action: Log (for audit)
```
### Detect Mixed Auth
```javascript
// Alert on inconsistent auth patterns
(cf.api_gateway.cf-risk-mixed-auth and http.host eq "api.example.com")
// Action: Log (review required)
```
## Fallthrough Detection (Shadow APIs)
```javascript
// WAF Custom Rule
(cf.api_gateway.fallthrough_triggered and http.host eq "api.example.com")
// Action: Log (discover unknown) or Block (strict)
```
## Rate Limiting by User
```javascript
// Rate Limiting Rule (modern syntax)
(http.host eq "api.example.com" and
is_jwt_valid(http.request.jwt.payload["{config_id}"][0]))
// Rate: 100 req/60s
// Counting expression: lookup_json_string(http.request.jwt.payload["{config_id}"][0], "sub")
```
## Volumetric Abuse Response
```javascript
// Detect abnormal traffic spikes
(cf.api_gateway.volumetric_abuse_detected and http.host eq "api.example.com")
// Action: Challenge or Rate Limit
// Combined with rate limiting
(cf.api_gateway.volumetric_abuse_detected or
cf.threat_score gt 50) and http.host eq "api.example.com"
// Action: JS Challenge
```
## GraphQL Protection
```javascript
// Block oversized queries
(http.request.uri.path eq "/graphql" and
cf.api_gateway.graphql_query_size gt 100000)
// Action: Block
// Block deep nested queries
(http.request.uri.path eq "/graphql" and
cf.api_gateway.graphql_query_depth gt 10)
// Action: Block
```
## Architecture Patterns
**Public API:** Discovery + Schema Validation 2.0 + JWT + Rate Limiting + Bot Management
**Partner API:** mTLS + Schema Validation + Sequence Mitigation
**Internal API:** Discovery + Schema Learning + Auth Posture
## OWASP API Security Top 10 Mapping (2026)
| OWASP Issue | API Shield Solutions |
|-------------|---------------------|
| API1:2023 Broken Object Level Authorization | **BOLA Detection** (enumeration + pollution), Sequence mitigation, Schema, JWT, Rate Limiting |
| API2:2023 Broken Authentication | **Auth Posture**, mTLS, JWT validation, Bot Management |
| API3:2023 Broken Object Property Auth | Schema validation, JWT validation |
| API4:2023 Unrestricted Resource Access | Rate Limiting, **Volumetric Abuse Detection**, **GraphQL Protection**, Bot Management |
| API5:2023 Broken Function Level Auth | Schema validation, JWT validation, Auth Posture |
| API6:2023 Unrestricted Business Flows | Sequence mitigation, Bot Management |
| API7:2023 SSRF | Schema validation, WAF managed rules |
| API8:2023 Security Misconfiguration | **Schema Validation 2.0**, Auth Posture, WAF rules |
| API9:2023 Improper Inventory Management | **API Discovery**, Schema learning, Auth Posture |
| API10:2023 Unsafe API Consumption | JWT validation, Schema validation, WAF managed |
## Monitoring
**Security Events:** `Security > Events` → Filter: Action = block, Service = API Shield
**Firewall Analytics:** `Analytics > Security` → Filter by `cf.api_gateway.*` fields
**Logpush fields:** APIGatewayAuthIDPresent, APIGatewayRequestViolatesSchema, APIGatewayFallthroughDetected, JWTValidationResult
## Availability (2026)
| Feature | Availability | Notes |
|---------|-------------|-------|
| mTLS (CF-managed CA) | All plans | Self-service |
| Endpoint Management | All plans | Limited operations |
| Schema Validation 2.0 | All plans | Limited operations |
| API Discovery | Enterprise | 10K+ ops |
| JWT Validation | Enterprise add-on | Full validation |
| BOLA Detection | Enterprise add-on | Requires session IDs |
| Auth Posture | Enterprise add-on | Security audit |
| Volumetric Abuse Detection | Enterprise add-on | Traffic analysis |
| GraphQL Protection | Enterprise add-on | Query limits |
| Sequence Mitigation | Enterprise (beta) | Contact team |
| Full Suite | Enterprise add-on | All features |
**Enterprise limits:** 10K operations (contact for higher). Preview access available for non-contract evaluation.
## See Also
- [configuration.md](configuration.md) - Setup all features before creating rules
- [api.md](api.md) - Firewall field reference and API endpoints
- [gotchas.md](gotchas.md) - Common issues and limits
@@ -0,0 +1,66 @@
# Cloudflare API Integration
Guide for working with Cloudflare's REST API - authentication, SDK usage, common patterns, and troubleshooting.
## Quick Decision Tree
```
How are you calling the Cloudflare API?
├─ From Workers runtime → Use bindings, not REST API (see ../bindings/)
├─ Server-side (Node/Python/Go) → Official SDK (see api.md)
├─ CLI/scripts → Wrangler or curl (see configuration.md)
├─ Infrastructure-as-code → See ../pulumi/ or ../terraform/
└─ One-off requests → curl examples (see api.md)
```
## SDK Selection
| Language | Package | Best For | Default Retries |
|----------|---------|----------|-----------------|
| TypeScript | `cloudflare` | Node.js, Bun, Next.js, Workers | 2 |
| Python | `cloudflare` | FastAPI, Django, scripts | 2 |
| Go | `cloudflare-go/v4` | CLI tools, microservices | 10 |
All SDKs are Stainless-generated from OpenAPI spec (consistent APIs).
## Authentication Methods
| Method | Security | Use Case | Scope |
|--------|----------|----------|-------|
| **API Token** ✓ | Scoped, rotatable | Production | Per-zone or account |
| API Key + Email | Full account access | Legacy only | Everything |
| User Service Key | Limited | Origin CA certs only | Origin CA |
**Always use API tokens** for new projects.
## Rate Limits
| Limit | Value |
|-------|-------|
| Per user/token | 1200 requests / 5 minutes |
| Per IP | 200 requests / second |
| GraphQL | 320 / 5 minutes (cost-based) |
## Reading Order
| Task | Files to Read |
|------|---------------|
| Initialize SDK client | api.md |
| Configure auth/timeout/retry | configuration.md |
| Find usage patterns | patterns.md |
| Debug errors/rate limits | gotchas.md |
| Product-specific APIs | ../workers/, ../r2/, ../kv/, etc. |
## In This Reference
- **[api.md](api.md)** - SDK client initialization, pagination, error handling, examples
- **[configuration.md](configuration.md)** - Environment variables, SDK config, Wrangler setup
- **[patterns.md](patterns.md)** - Real-world patterns, batch operations, workflows
- **[gotchas.md](gotchas.md)** - Rate limits, SDK-specific issues, troubleshooting
## See Also
- [Cloudflare API Docs](https://developers.cloudflare.com/api/)
- [Bindings Reference](../bindings/) - Workers runtime bindings (preferred over REST API)
- [Wrangler Reference](../wrangler/) - CLI tool for Cloudflare development
- [GraphQL Analytics API Reference](../graphql-api/) - Analytics data via GraphQL (separate endpoint from REST API)
@@ -0,0 +1,204 @@
# API Reference
## Client Initialization
### TypeScript
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
});
```
### Python
```python
from cloudflare import Cloudflare
client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN"))
# For async:
from cloudflare import AsyncCloudflare
client = AsyncCloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"])
```
### Go
```go
import (
"github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/option"
)
client := cloudflare.NewClient(
option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")),
)
```
## Authentication
### API Token (Recommended)
**Create token**: Dashboard → My Profile → API Tokens → Create Token
```bash
export CLOUDFLARE_API_TOKEN='your-token-here'
curl "https://api.cloudflare.com/client/v4/zones" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
```
**Token scopes**: Always use minimal permissions (zone-specific, time-limited).
### API Key (Legacy)
```bash
curl "https://api.cloudflare.com/client/v4/zones" \
--header "X-Auth-Email: user@example.com" \
--header "X-Auth-Key: $CLOUDFLARE_API_KEY"
```
**Not recommended:** Full account access, cannot scope permissions.
## Auto-Pagination
All SDKs support automatic pagination for list operations.
```typescript
// TypeScript: for await...of
for await (const zone of client.zones.list()) {
console.log(zone.id);
}
```
```python
# Python: iterator protocol
for zone in client.zones.list():
print(zone.id)
```
```go
// Go: ListAutoPaging
iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})
for iter.Next() {
zone := iter.Current()
fmt.Println(zone.ID)
}
```
## Error Handling
```typescript
try {
const zone = await client.zones.get({ zone_id: 'xxx' });
} catch (err) {
if (err instanceof Cloudflare.NotFoundError) {
// 404
} else if (err instanceof Cloudflare.RateLimitError) {
// 429 - SDK auto-retries with backoff
} else if (err instanceof Cloudflare.APIError) {
console.log(err.status, err.message);
}
}
```
**Common Error Types:**
- `AuthenticationError` (401) - Invalid token
- `PermissionDeniedError` (403) - Insufficient scope
- `NotFoundError` (404) - Resource not found
- `RateLimitError` (429) - Rate limit exceeded
- `InternalServerError` (≥500) - Cloudflare error
## Zone Management
```typescript
// List zones
const zones = await client.zones.list({
account: { id: 'account-id' },
status: 'active',
});
// Create zone
const zone = await client.zones.create({
account: { id: 'account-id' },
name: 'example.com',
type: 'full', // or 'partial'
});
// Update zone
await client.zones.edit('zone-id', {
paused: false,
});
// Delete zone
await client.zones.delete('zone-id');
```
```go
// Go: requires cloudflare.F() wrapper
zone, err := client.Zones.New(ctx, cloudflare.ZoneNewParams{
Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{
ID: cloudflare.F("account-id"),
}),
Name: cloudflare.F("example.com"),
Type: cloudflare.F(cloudflare.ZoneNewParamsTypeFull),
})
```
## DNS Management
```typescript
// Create DNS record
await client.dns.records.create({
zone_id: 'zone-id',
type: 'A',
name: 'subdomain.example.com',
content: '192.0.2.1',
ttl: 1, // auto
proxied: true, // Orange cloud
});
// List DNS records (with auto-pagination)
for await (const record of client.dns.records.list({
zone_id: 'zone-id',
type: 'A',
})) {
console.log(record.name, record.content);
}
// Update DNS record
await client.dns.records.update({
zone_id: 'zone-id',
dns_record_id: 'record-id',
type: 'A',
name: 'subdomain.example.com',
content: '203.0.113.1',
proxied: true,
});
// Delete DNS record
await client.dns.records.delete({
zone_id: 'zone-id',
dns_record_id: 'record-id',
});
```
```python
# Python example
client.dns.records.create(
zone_id="zone-id",
type="A",
name="subdomain.example.com",
content="192.0.2.1",
ttl=1,
proxied=True,
)
```
## See Also
- [configuration.md](./configuration.md) - SDK configuration, environment variables
- [patterns.md](./patterns.md) - Real-world patterns and workflows
- [gotchas.md](./gotchas.md) - Rate limits, troubleshooting
@@ -0,0 +1,160 @@
# Configuration
## Environment Variables
### Set Variables
| Platform | Command |
|----------|---------|
| Linux/macOS | `export CLOUDFLARE_API_TOKEN='token'` |
| PowerShell | `$env:CLOUDFLARE_API_TOKEN = 'token'` |
| Windows CMD | `set CLOUDFLARE_API_TOKEN=token` |
**Security:** Never commit tokens. Use `.env` files (gitignored) or secret managers.
### .env File Pattern
```bash
# .env (add to .gitignore)
CLOUDFLARE_API_TOKEN=your-token-here
CLOUDFLARE_ACCOUNT_ID=your-account-id
```
```typescript
// TypeScript
import 'dotenv/config';
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
});
```
```python
# Python
from dotenv import load_dotenv
load_dotenv()
client = Cloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"])
```
## SDK Configuration
### TypeScript
```typescript
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
timeout: 120000, // 2 min (default 60s), in milliseconds
maxRetries: 5, // default 2
baseURL: 'https://...', // proxy (rare)
});
// Per-request overrides
await client.zones.get(
{ zone_id: 'zone-id' },
{ timeout: 5000, maxRetries: 0 }
);
```
### Python
```python
client = Cloudflare(
api_token=os.environ["CLOUDFLARE_API_TOKEN"],
timeout=120, # seconds (default 60)
max_retries=5, # default 2
base_url="https://...", # proxy (rare)
)
# Per-request overrides
client.with_options(timeout=5, max_retries=0).zones.get(zone_id="zone-id")
```
### Go
```go
client := cloudflare.NewClient(
option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")),
option.WithMaxRetries(5), // default 10 (higher than TS/Python)
option.WithRequestTimeout(2 * time.Minute), // default 60s
option.WithBaseURL("https://..."), // proxy (rare)
)
// Per-request overrides
client.Zones.Get(ctx, "zone-id", option.WithMaxRetries(0))
```
## Configuration Options
| Option | TypeScript | Python | Go | Default |
|--------|-----------|--------|-----|---------|
| Timeout | `timeout` (ms) | `timeout` (s) | `WithRequestTimeout` | 60s |
| Retries | `maxRetries` | `max_retries` | `WithMaxRetries` | 2 (Go: 10) |
| Base URL | `baseURL` | `base_url` | `WithBaseURL` | api.cloudflare.com |
**Note:** Go SDK has higher default retries (10) than TypeScript/Python (2).
## Timeout Configuration
**When to increase:**
- Large zone transfers
- Bulk DNS operations
- Worker script uploads
```typescript
const client = new Cloudflare({
timeout: 300000, // 5 minutes
});
```
## Retry Configuration
**When to increase:** Rate-limit-heavy workflows, flaky network
**When to decrease:** Fast-fail requirements, user-facing requests
```typescript
// Increase retries for batch operations
const client = new Cloudflare({ maxRetries: 10 });
// Disable retries for fast-fail
const fastClient = new Cloudflare({ maxRetries: 0 });
```
## Wrangler CLI Integration
```bash
# Configure authentication
wrangler login
# Or
export CLOUDFLARE_API_TOKEN='token'
# Common commands that use API
wrangler deploy # Uploads worker via API
wrangler kv:key put # KV operations
wrangler r2 bucket create # R2 operations
wrangler d1 execute # D1 operations
wrangler pages deploy # Pages operations
# Get API configuration
wrangler whoami # Shows authenticated user
```
### wrangler.toml
```toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
account_id = "your-account-id"
# Can also use env vars:
# CLOUDFLARE_ACCOUNT_ID
# CLOUDFLARE_API_TOKEN
```
## See Also
- [api.md](./api.md) - Client initialization, authentication
- [gotchas.md](./gotchas.md) - Rate limits, timeout errors
- [Wrangler Reference](../wrangler/) - CLI tool details
@@ -0,0 +1,225 @@
# Gotchas & Troubleshooting
## Rate Limits & 429 Errors
**Actual Limits:**
- **1200 requests / 5 minutes** per user/token (global)
- **200 requests / second** per IP address
- **GraphQL: 320 / 5 minutes** (cost-based)
**SDK Behavior:**
- Auto-retry with exponential backoff (default 2 retries, Go: 10)
- Respects `Retry-After` header
- Throws `RateLimitError` after exhausting retries
**Solution:**
```typescript
// Increase retries for rate-limit-heavy workflows
const client = new Cloudflare({ maxRetries: 5 });
// Add application-level throttling
import pLimit from 'p-limit';
const limit = pLimit(10); // Max 10 concurrent requests
```
## SDK-Specific Issues
### Go: Required Field Wrapper
**Problem:** Go SDK requires `cloudflare.F()` wrapper for optional fields.
```go
// ❌ WRONG - Won't compile or send field
client.Zones.New(ctx, cloudflare.ZoneNewParams{
Name: "example.com",
})
// ✅ CORRECT
client.Zones.New(ctx, cloudflare.ZoneNewParams{
Name: cloudflare.F("example.com"),
Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{
ID: cloudflare.F("account-id"),
}),
})
```
**Why:** Distinguishes between zero value, null, and omitted fields.
### Python: Async vs Sync Clients
**Problem:** Using sync client in async context or vice versa.
```python
# ❌ WRONG - Can't await sync client
from cloudflare import Cloudflare
client = Cloudflare()
await client.zones.list() # TypeError
# ✅ CORRECT - Use AsyncCloudflare
from cloudflare import AsyncCloudflare
client = AsyncCloudflare()
await client.zones.list()
```
## Token Permission Errors (403)
**Problem:** API returns 403 Forbidden despite valid token.
**Cause:** Token lacks required permissions (scope).
**Scopes Required:**
| Operation | Required Scope |
|-----------|----------------|
| List zones | Zone:Read (zone-level or account-level) |
| Create zone | Zone:Edit (account-level) |
| Edit DNS | DNS:Edit (zone-level) |
| Deploy Worker | Workers Script:Edit (account-level) |
| Read KV | Workers KV Storage:Read |
| Write KV | Workers KV Storage:Edit |
**Solution:** Re-create token with correct permissions in Dashboard → My Profile → API Tokens.
## Pagination Truncation
**Problem:** Only getting first 20 results (default page size).
**Solution:** Use auto-pagination iterators.
```typescript
// ❌ WRONG - Only first page (20 items)
const page = await client.zones.list();
// ✅ CORRECT - All results
const zones = [];
for await (const zone of client.zones.list()) {
zones.push(zone);
}
```
## Workers Subrequests
**Problem:** Rate limit hit faster than expected in Workers.
**Cause:** Workers subrequests count as separate API calls.
**Solution:** Use bindings instead of REST API in Workers (see ../bindings/).
```typescript
// ❌ WRONG - REST API in Workers (counts against rate limit)
const client = new Cloudflare({ apiToken: env.CLOUDFLARE_API_TOKEN });
const zones = await client.zones.list();
// ✅ CORRECT - Use bindings (no rate limit)
// Access via env.MY_BINDING
```
## Authentication Errors (401)
**Problem:** "Authentication failed" or "Invalid token"
**Causes:**
- Token expired
- Token deleted/revoked
- Token not set in environment
- Wrong token format
**Solution:**
```typescript
// Verify token is set
if (!process.env.CLOUDFLARE_API_TOKEN) {
throw new Error('CLOUDFLARE_API_TOKEN not set');
}
// Test token
const user = await client.user.tokens.verify();
console.log('Token valid:', user.status);
```
## Timeout Errors
**Problem:** Request times out (default 60s).
**Cause:** Large operations (bulk DNS, zone transfers).
**Solution:** Increase timeout or split operations.
```typescript
// Increase timeout
const client = new Cloudflare({
timeout: 300000, // 5 minutes
});
// Or split operations
const batchSize = 100;
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
await processBatch(batch);
}
```
## Zone Not Found (404)
**Problem:** Zone ID valid but returns 404.
**Causes:**
- Zone not in account associated with token
- Zone deleted
- Wrong zone ID format
**Solution:**
```typescript
// List all zones to find correct ID
for await (const zone of client.zones.list()) {
console.log(zone.id, zone.name);
}
```
## Limits Reference
| Resource/Limit | Value | Notes |
|----------------|-------|-------|
| API rate limit | 1200/5min | Per user/token |
| IP rate limit | 200/sec | Per IP |
| GraphQL rate limit | 320/5min | Cost-based |
| Parallel requests (recommended) | < 10 | Avoid overwhelming API |
| Default page size | 20 | Use auto-pagination |
| Max page size | 50 | Some endpoints |
## Best Practices
**Security:**
- Never commit tokens
- Use minimal permissions
- Rotate tokens regularly
- Set token expiration
**Performance:**
- Batch operations
- Use pagination wisely
- Cache responses
- Handle rate limits
**Code Organization:**
```typescript
// Create reusable client instance
export const cfClient = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
maxRetries: 5,
});
// Wrap common operations
export async function getZoneDetails(zoneId: string) {
return await cfClient.zones.get({ zone_id: zoneId });
}
```
## See Also
- [api.md](./api.md) - Error types, authentication
- [configuration.md](./configuration.md) - Timeout/retry configuration
- [patterns.md](./patterns.md) - Error handling patterns
@@ -0,0 +1,204 @@
# Common Patterns
## List All with Auto-Pagination
**Problem:** API returns paginated results. Default page size is 20.
**Solution:** Use SDK auto-pagination to iterate all results.
```typescript
// TypeScript
for await (const zone of client.zones.list()) {
console.log(zone.name);
}
```
```python
# Python
for zone in client.zones.list():
print(zone.name)
```
```go
// Go
iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})
for iter.Next() {
fmt.Println(iter.Current().Name)
}
```
## Error Handling with Retry
**Problem:** Rate limits (429) and transient errors need retry.
**Solution:** SDKs auto-retry with exponential backoff. Customize as needed.
```typescript
// Increase retries for rate-limit-heavy operations
const client = new Cloudflare({ maxRetries: 5 });
try {
const zone = await client.zones.create({ /* ... */ });
} catch (err) {
if (err instanceof Cloudflare.RateLimitError) {
// Already retried 5 times with backoff
const retryAfter = err.headers['retry-after'];
console.log(`Rate limited. Retry after ${retryAfter}s`);
}
}
```
## Batch Parallel Operations
**Problem:** Need to create multiple resources quickly.
**Solution:** Use `Promise.all()` for parallel requests (respect rate limits).
```typescript
// Create multiple DNS records in parallel
const records = ['www', 'api', 'cdn'].map(subdomain =>
client.dns.records.create({
zone_id: 'zone-id',
type: 'A',
name: `${subdomain}.example.com`,
content: '192.0.2.1',
})
);
await Promise.all(records);
```
**Controlled concurrency** (avoid rate limits):
```typescript
import pLimit from 'p-limit';
const limit = pLimit(10); // Max 10 concurrent
const subdomains = ['www', 'api', 'cdn', /* many more */];
const records = subdomains.map(subdomain =>
limit(() => client.dns.records.create({
zone_id: 'zone-id',
type: 'A',
name: `${subdomain}.example.com`,
content: '192.0.2.1',
}))
);
await Promise.all(records);
```
## Zone CRUD Workflow
```typescript
// Create
const zone = await client.zones.create({
account: { id: 'account-id' },
name: 'example.com',
type: 'full',
});
// Read
const fetched = await client.zones.get({ zone_id: zone.id });
// Update
await client.zones.edit(zone.id, { paused: false });
// Delete
await client.zones.delete(zone.id);
```
## DNS Bulk Update
```typescript
// Fetch all A records
const records = [];
for await (const record of client.dns.records.list({
zone_id: 'zone-id',
type: 'A',
})) {
records.push(record);
}
// Update all to new IP
await Promise.all(records.map(record =>
client.dns.records.update({
zone_id: 'zone-id',
dns_record_id: record.id,
type: 'A',
name: record.name,
content: '203.0.113.1', // New IP
proxied: record.proxied,
ttl: record.ttl,
})
));
```
## Filter and Collect Results
```typescript
// Find all proxied A records
const proxiedRecords = [];
for await (const record of client.dns.records.list({
zone_id: 'zone-id',
type: 'A',
})) {
if (record.proxied) {
proxiedRecords.push(record);
}
}
```
## Error Recovery Pattern
```typescript
async function createZoneWithRetry(name: string, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await client.zones.create({
account: { id: 'account-id' },
name,
type: 'full',
});
} catch (err) {
if (err instanceof Cloudflare.RateLimitError && attempt < maxAttempts) {
const retryAfter = parseInt(err.headers['retry-after'] || '5');
console.log(`Rate limited, waiting ${retryAfter}s (retry ${attempt}/${maxAttempts})`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
} else {
throw err;
}
}
}
}
```
## Conditional Update Pattern
```typescript
// Only update if zone is active
const zone = await client.zones.get({ zone_id: 'zone-id' });
if (zone.status === 'active') {
await client.zones.edit(zone.id, { paused: false });
}
```
## Batch with Error Handling
```typescript
// Process multiple zones, continue on errors
const results = await Promise.allSettled(
zoneIds.map(id => client.zones.get({ zone_id: id }))
);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Zone ${i}: ${result.value.name}`);
} else {
console.error(`Zone ${i} failed:`, result.reason.message);
}
});
```
## See Also
- [api.md](./api.md) - SDK client initialization, basic operations
- [gotchas.md](./gotchas.md) - Rate limits, common errors
- [configuration.md](./configuration.md) - SDK configuration options
@@ -0,0 +1,90 @@
# Cloudflare Argo Smart Routing Skill Reference
## Overview
Cloudflare Argo Smart Routing is a performance optimization service that detects real-time network issues and routes web traffic across the most efficient network path. It continuously monitors network conditions and intelligently routes traffic through the fastest, most reliable routes in Cloudflare's network.
**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product for enhanced DDoS protection and performance. Existing Argo customers maintain full functionality with gradual migration to Smart Shield features.
## Quick Start
### Enable via cURL
```bash
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "on"}'
```
### Enable via TypeScript SDK
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });
const result = await client.argo.smartRouting.edit({
zone_id: 'your-zone-id',
value: 'on',
});
console.log(`Argo enabled: ${result.value}`);
```
## Core Concepts
### What It Does
- **Intelligent routing**: Detects congestion, outages, packet loss in real-time
- **Global optimization**: Routes across 300+ Cloudflare data centers
- **Automatic failover**: Switches paths when issues detected (typically <1s)
- **Works with existing setup**: No origin changes required
### Billing Model
- Usage-based: Charged per GB of traffic (excluding DDoS/WAF mitigated traffic)
- Requires billing configuration before enabling
- Available on Enterprise+ plans (check zone eligibility)
### When to Use
- **High-traffic production sites** with global user base
- **Latency-sensitive applications** (APIs, real-time services)
- **Sites behind Cloudflare proxy** (orange-clouded DNS records)
- **Combined with Tiered Cache** for maximum performance gains
### When NOT to Use
- Development/staging environments (cost control)
- Low-traffic sites (<1TB/month) where cost may exceed benefit
- Sites with primarily single-region traffic
## Should I Enable Argo?
| Your Situation | Recommendation |
|----------------|----------------|
| Global production app, >1TB/month traffic | ✅ Enable - likely ROI positive |
| Enterprise plan, latency-critical APIs | ✅ Enable - performance matters |
| Regional site, <100GB/month traffic | ⚠️ Evaluate - cost may not justify |
| Development/staging environment | ❌ Disable - use in production only |
| Not yet configured billing | ❌ Configure billing first |
## Reading Order by Task
| Your Goal | Start With | Then Read |
|-----------|------------|-----------|
| Enable Argo for first time | Quick Start above → [configuration.md](configuration.md) | [gotchas.md](gotchas.md) |
| Use TypeScript/Python SDK | [api.md](api.md) | [patterns.md](patterns.md) |
| Terraform/IaC setup | [configuration.md](configuration.md) | - |
| Enable for Spectrum TCP app | [patterns.md](patterns.md) → Spectrum section | [api.md](api.md) |
| Troubleshoot enablement issue | [gotchas.md](gotchas.md) | [api.md](api.md) |
| Manage billing/usage | [patterns.md](patterns.md) → Billing section | [gotchas.md](gotchas.md) |
## In This Reference
- **[api.md](api.md)** - API endpoints, SDK methods, error handling, Python/TypeScript examples
- **[configuration.md](configuration.md)** - Terraform setup, environment config, billing configuration
- **[patterns.md](patterns.md)** - Tiered Cache integration, Spectrum TCP apps, billing management, validation patterns
- **[gotchas.md](gotchas.md)** - Common errors, permission issues, limits, best practices
## See Also
- [Cloudflare Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)
- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)
- [Spectrum Documentation](https://developers.cloudflare.com/spectrum/)
- [Tiered Cache](https://developers.cloudflare.com/cache/how-to/tiered-cache/)
@@ -0,0 +1,240 @@
## API Reference
**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product. API endpoints remain stable; existing integrations continue to work without changes.
### Base Endpoint
```
https://api.cloudflare.com/client/v4
```
### Authentication
Use API tokens with Zone:Argo Smart Routing:Edit permissions:
```bash
# Headers required
X-Auth-Email: user@example.com
Authorization: Bearer YOUR_API_TOKEN
```
### Get Argo Smart Routing Status
**Endpoint:** `GET /zones/{zone_id}/argo/smart_routing`
**Description:** Retrieves current Argo Smart Routing enablement status.
**cURL Example:**
```bash
curl -X GET "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json"
```
**Response:**
```json
{
"result": {
"id": "smart_routing",
"value": "on",
"editable": true,
"modified_on": "2024-01-11T12:00:00Z"
},
"success": true,
"errors": [],
"messages": []
}
```
**TypeScript SDK Example:**
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN
});
const status = await client.argo.smartRouting.get({ zone_id: 'your-zone-id' });
console.log(`Argo status: ${status.value}, editable: ${status.editable}`);
```
**Python SDK Example:**
```python
from cloudflare import Cloudflare
client = Cloudflare(api_token=os.environ.get('CLOUDFLARE_API_TOKEN'))
status = client.argo.smart_routing.get(zone_id='your-zone-id')
print(f"Argo status: {status.value}, editable: {status.editable}")
```
### Update Argo Smart Routing Status
**Endpoint:** `PATCH /zones/{zone_id}/argo/smart_routing`
**Description:** Enable or disable Argo Smart Routing for a zone.
**Request Body:**
```json
{
"value": "on" // or "off"
}
```
**cURL Example:**
```bash
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "on"}'
```
**TypeScript SDK Example:**
```typescript
const result = await client.argo.smartRouting.edit({
zone_id: 'your-zone-id',
value: 'on',
});
console.log(`Updated: ${result.value} at ${result.modified_on}`);
```
**Python SDK Example:**
```python
result = client.argo.smart_routing.edit(
zone_id='your-zone-id',
value='on'
)
print(f"Updated: {result.value} at {result.modified_on}")
```
## Checking Editability Before Updates
**Critical:** Always check the `editable` field before attempting to enable/disable Argo. When `editable: false`, the zone has restrictions (billing not configured, insufficient permissions, or plan limitations).
**Pattern:**
```typescript
async function safelyEnableArgo(client: Cloudflare, zoneId: string): Promise<boolean> {
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
if (!status.editable) {
console.error('Cannot modify Argo: editable=false (check billing/permissions)');
return false;
}
if (status.value === 'on') {
console.log('Argo already enabled');
return true;
}
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
console.log('Argo enabled successfully');
return true;
}
```
**Python Pattern:**
```python
def safely_enable_argo(client: Cloudflare, zone_id: str) -> bool:
status = client.argo.smart_routing.get(zone_id=zone_id)
if not status.editable:
print('Cannot modify Argo: editable=false (check billing/permissions)')
return False
if status.value == 'on':
print('Argo already enabled')
return True
client.argo.smart_routing.edit(zone_id=zone_id, value='on')
print('Argo enabled successfully')
return True
```
## Error Handling
The TypeScript SDK provides typed error classes for robust error handling:
```typescript
import Cloudflare from 'cloudflare';
import { APIError, APIConnectionError, RateLimitError } from 'cloudflare';
async function enableArgoWithErrorHandling(client: Cloudflare, zoneId: string) {
try {
const result = await client.argo.smartRouting.edit({
zone_id: zoneId,
value: 'on',
});
return result;
} catch (error) {
if (error instanceof RateLimitError) {
console.error('Rate limited. Retry after:', error.response?.headers.get('retry-after'));
// Implement exponential backoff
} else if (error instanceof APIError) {
console.error('API error:', error.status, error.message);
if (error.status === 403) {
console.error('Permission denied - check API token scopes');
} else if (error.status === 400) {
console.error('Bad request - verify zone_id and payload');
}
} else if (error instanceof APIConnectionError) {
console.error('Connection failed:', error.message);
// Retry with exponential backoff
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
```
**Python Error Handling:**
```python
from cloudflare import Cloudflare, APIError, RateLimitError
def enable_argo_with_error_handling(client: Cloudflare, zone_id: str):
try:
result = client.argo.smart_routing.edit(zone_id=zone_id, value='on')
return result
except RateLimitError as e:
print(f"Rate limited. Retry after: {e.response.headers.get('retry-after')}")
raise
except APIError as e:
print(f"API error: {e.status} - {e.message}")
if e.status == 403:
print('Permission denied - check API token scopes')
elif e.status == 400:
print('Bad request - verify zone_id and payload')
raise
except Exception as e:
print(f"Unexpected error: {e}")
raise
```
## Response Schema
All Argo Smart Routing API responses follow this structure:
```typescript
interface ArgoSmartRoutingResponse {
result: {
id: 'smart_routing';
value: 'on' | 'off';
editable: boolean;
modified_on: string; // ISO 8601 timestamp
};
success: boolean;
errors: Array<{
code: number;
message: string;
}>;
messages: Array<string>;
}
```
## Key Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `value` | `"on" \| "off"` | Current enablement status |
| `editable` | `boolean` | Whether changes are allowed (check before PATCH) |
| `modified_on` | `string` | ISO timestamp of last modification |
| `success` | `boolean` | Whether request succeeded |
| `errors` | `Array` | Error details if `success: false`
@@ -0,0 +1,197 @@
## Configuration Management
**Note on Smart Shield Evolution:** Argo Smart Routing is being integrated into Smart Shield. Configuration methods below remain valid; Terraform and IaC patterns unchanged.
### Infrastructure as Code (Terraform)
```hcl
# terraform/argo.tf
# Note: Use Cloudflare Terraform provider
resource "cloudflare_argo" "example" {
zone_id = var.zone_id
smart_routing = "on"
tiered_caching = "on"
}
variable "zone_id" {
description = "Cloudflare Zone ID"
type = string
}
output "argo_enabled" {
value = cloudflare_argo.example.smart_routing
description = "Argo Smart Routing status"
}
```
### Environment-Based Configuration
```typescript
// config/argo.ts
interface ArgoEnvironmentConfig {
enabled: boolean;
tieredCache: boolean;
monitoring: {
usageAlerts: boolean;
threshold: number;
};
}
const configs: Record<string, ArgoEnvironmentConfig> = {
production: {
enabled: true,
tieredCache: true,
monitoring: {
usageAlerts: true,
threshold: 1000, // GB
},
},
staging: {
enabled: true,
tieredCache: false,
monitoring: {
usageAlerts: false,
threshold: 100, // GB
},
},
development: {
enabled: false,
tieredCache: false,
monitoring: {
usageAlerts: false,
threshold: 0,
},
},
};
export function getArgoConfig(env: string): ArgoEnvironmentConfig {
return configs[env] || configs.development;
}
```
### Pulumi Configuration
```typescript
// pulumi/argo.ts
import * as cloudflare from '@pulumi/cloudflare';
const zone = new cloudflare.Zone('example-zone', {
zone: 'example.com',
plan: 'enterprise',
});
const argoSettings = new cloudflare.Argo('argo-config', {
zoneId: zone.id,
smartRouting: 'on',
tieredCaching: 'on',
});
export const argoEnabled = argoSettings.smartRouting;
export const zoneId = zone.id;
```
## Billing Configuration
Before enabling Argo Smart Routing, ensure billing is configured for the account:
**Prerequisites:**
1. Valid payment method on file
2. Enterprise or higher plan
3. Zone must have billing enabled
**Check Billing Status via Dashboard:**
1. Navigate to Account → Billing
2. Verify payment method configured
3. Check zone subscription status
**Note:** Attempting to enable Argo without billing configured will result in `editable: false` in API responses.
## Environment Variable Setup
**Required Environment Variables:**
```bash
# .env
CLOUDFLARE_API_TOKEN=your_api_token_here
CLOUDFLARE_ZONE_ID=your_zone_id_here
CLOUDFLARE_ACCOUNT_ID=your_account_id_here
# Optional
ARGO_ENABLED=true
ARGO_TIERED_CACHE=true
```
**TypeScript Configuration Loader:**
```typescript
// config/env.ts
import { z } from 'zod';
const envSchema = z.object({
CLOUDFLARE_API_TOKEN: z.string().min(1),
CLOUDFLARE_ZONE_ID: z.string().min(1),
CLOUDFLARE_ACCOUNT_ID: z.string().min(1),
ARGO_ENABLED: z.string().optional().default('false'),
ARGO_TIERED_CACHE: z.string().optional().default('false'),
});
export const env = envSchema.parse(process.env);
export const argoConfig = {
enabled: env.ARGO_ENABLED === 'true',
tieredCache: env.ARGO_TIERED_CACHE === 'true',
};
```
## CI/CD Integration
**GitHub Actions Example:**
```yaml
# .github/workflows/deploy-argo.yml
name: Deploy Argo Configuration
on:
push:
branches: [main]
paths:
- 'terraform/argo.tf'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
- name: Terraform Init
run: terraform init
working-directory: ./terraform
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: ./terraform
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TF_VAR_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
```
## Enterprise Preview Program
For early access to Argo Smart Routing features and Smart Shield integration:
**Eligibility:**
- Enterprise plan customers
- Active Cloudflare support contract
- Production traffic >100GB/month
**How to Join:**
1. Contact Cloudflare account team or support
2. Request Argo/Smart Shield preview access
3. Receive preview zone configuration
**Preview Features:**
- Enhanced analytics and reporting
- Smart Shield DDoS integration
- Advanced routing policies
- Priority support for routing issues
@@ -0,0 +1,111 @@
## Best Practices Summary
**Smart Shield Note:** Argo Smart Routing evolving into Smart Shield. Best practices below remain applicable; monitor Cloudflare changelog for Smart Shield updates.
1. **Always check editability** before attempting to enable/disable Argo
2. **Set up billing notifications** to avoid unexpected costs
3. **Combine with Tiered Cache** for maximum performance benefit
4. **Use in production only** - disable for dev/staging to control costs
5. **Monitor analytics** - require 500+ requests in 48h for detailed metrics
6. **Handle errors gracefully** - check for billing, permissions, zone compatibility
7. **Test configuration changes** in staging before production
8. **Use TypeScript SDK** for type safety and better developer experience
9. **Implement retry logic** for API calls in production systems
10. **Document zone-specific settings** for team visibility
## Common Errors
### "Argo unavailable"
**Problem:** API returns error "Argo Smart Routing is unavailable for this zone"
**Cause:** Zone not eligible or billing not set up
**Solution:**
1. Verify zone has Enterprise or higher plan
2. Check billing is configured in Account → Billing
3. Ensure payment method is valid and current
4. Contact Cloudflare support if eligibility unclear
### "Cannot enable/disable"
**Problem:** API call succeeds but status remains unchanged, or `editable: false` in GET response
**Cause:** Insufficient permissions or zone restrictions
**Solution:**
1. Check API token has `Zone:Argo Smart Routing:Edit` permission
2. Verify `editable: true` in GET response before attempting PATCH
3. If `editable: false`, check:
- Billing configured for account
- Zone plan includes Argo (Enterprise+)
- No active zone holds or suspensions
- API token has correct scopes
### `editable: false` Error
**Problem:** GET request returns `"editable": false`, preventing enable/disable
**Cause:** Zone-level restrictions from billing, plan, or permissions
**Solution Pattern:**
```typescript
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
if (!status.editable) {
// Don't attempt to modify - will fail
console.error('Cannot modify Argo settings:');
console.error('- Check billing is configured');
console.error('- Verify zone has Enterprise+ plan');
console.error('- Confirm API token has Edit permission');
throw new Error('Argo is not editable for this zone');
}
// Safe to proceed with enable/disable
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
```
### Rate Limiting
**Problem:** `429 Too Many Requests` error from API
**Cause:** Exceeded API rate limits (typically 1200 requests per 5 minutes)
**Solution:**
```typescript
import { RateLimitError } from 'cloudflare';
try {
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
} catch (error) {
if (error instanceof RateLimitError) {
const retryAfter = error.response?.headers.get('retry-after');
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
// Implement exponential backoff
await new Promise(resolve => setTimeout(resolve, (retryAfter || 60) * 1000));
// Retry request
}
}
```
## Limits
| Resource/Limit | Value | Notes |
|----------------|-------|-------|
| Min requests for analytics | 500 in 48h | For detailed metrics via GraphQL |
| Zones supported | Enterprise+ | Check zone plan in dashboard |
| Billing requirement | Must be configured | Before enabling; verify payment method |
| API rate limit | 1200 req / 5 min | Per API token across all endpoints |
| Spectrum apps | No hard limit | Each app can enable Argo independently |
| Traffic counting | Proxied only | Only orange-clouded DNS records count |
| DDoS/WAF exemption | Yes | Mitigated traffic excluded from billing |
| Analytics latency | 1-5 minutes | Real-time metrics not available |
## Additional Resources
- [Official Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)
- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)
- [API Authentication](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)
- [Cloudflare TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript)
- [Cloudflare Python SDK](https://github.com/cloudflare/cloudflare-python)
@@ -0,0 +1,104 @@
# Integration Patterns
## Enable Argo + Tiered Cache
```typescript
async function enableOptimalPerformance(client: Cloudflare, zoneId: string) {
await Promise.all([
client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),
client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),
]);
}
```
**Flow:** Visitor → Edge (Lower-Tier) → [Cache Miss] → Upper-Tier → [Cache Miss + Argo] → Origin
**Impact:** Argo ~30% latency reduction + Tiered Cache 50-80% origin offload
## Usage Analytics (GraphQL)
```graphql
query ArgoAnalytics($zoneTag: string!) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
httpRequestsAdaptiveGroups(limit: 1000) {
sum { argoBytes, bytes }
}
}
}
}
```
**Billing:** ~$0.10/GB. DDoS-mitigated and WAF-blocked traffic NOT charged.
## Spectrum TCP Integration
Enable Argo for non-HTTP traffic (databases, game servers, IoT):
```typescript
// Update existing app
await client.spectrum.apps.update(appId, { zone_id: zoneId, argo_smart_routing: true });
// Create new app with Argo
await client.spectrum.apps.create({
zone_id: zoneId,
dns: { type: 'CNAME', name: 'tcp.example.com' },
origin_direct: ['tcp://origin.example.com:3306'],
protocol: 'tcp/3306',
argo_smart_routing: true,
});
```
**Use cases:** MySQL/PostgreSQL (3306/5432), game servers, MQTT (1883), SSH (22)
## Pre-Flight Validation
```typescript
async function validateArgoEligibility(client: Cloudflare, zoneId: string) {
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
const zone = await client.zones.get({ zone_id: zoneId });
const issues: string[] = [];
if (!status.editable) issues.push('Zone not editable');
if (['free', 'pro'].includes(zone.plan.legacy_id)) issues.push('Requires Business+ plan');
if (zone.status !== 'active') issues.push('Zone not active');
return { canEnable: issues.length === 0, issues };
}
```
## Post-Enable Verification
```typescript
async function verifyArgoEnabled(client: Cloudflare, zoneId: string): Promise<boolean> {
await new Promise(r => setTimeout(r, 2000)); // Wait for propagation
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
return status.value === 'on';
}
```
## Full Setup Pattern
```typescript
async function setupArgo(client: Cloudflare, zoneId: string) {
// 1. Validate
const { canEnable, issues } = await validateArgoEligibility(client, zoneId);
if (!canEnable) throw new Error(issues.join(', '));
// 2. Enable both features
await Promise.all([
client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),
client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),
]);
// 3. Verify
const [argo, cache] = await Promise.all([
client.argo.smartRouting.get({ zone_id: zoneId }),
client.argo.tieredCaching.get({ zone_id: zoneId }),
]);
return { argo: argo.value === 'on', tieredCache: cache.value === 'on' };
}
```
**When to combine:** High-traffic sites (>1TB/mo), global users, cacheable content.
@@ -0,0 +1,79 @@
# Cloudflare Artifacts
Store versioned file trees behind a repo-style interface that works from Workers, the REST API, and Git-compatible tooling.
## Overview
Use **Artifacts** when the thing you need to store is a versioned filesystem tree rather than a single object, key, or SQL row.
Typical Artifacts use cases:
- Git-style repositories
- Per-agent, per-session, or per-task repos
- Build outputs and deployment bundles
- Checkpoints and generated assets
- Shared file trees passed between developer tools and Workers
Artifacts is a good fit when the same content needs to be addressable from **Workers**, the **REST API**, and **Git-compatible clients**.
Artifacts is especially useful for agent and automation workflows where each unit of work should have its own isolated repo and token.
**Prefer retrieval over memory** for current availability, authentication details, route shapes, limits, and pricing. Start at `https://developers.cloudflare.com/artifacts/`.
## When to Use Artifacts
| Need | Use | Why |
|------|-----|-----|
| Versioned file trees such as repos, build outputs, checkpoints, or generated assets | Artifacts | Artifacts stores and shares **versioned filesystem content** |
| A git-compatible workflow with `clone`, `fetch`, `pull`, or `push` | Artifacts | Artifacts exposes **git-over-HTTPS remotes** and repo-scoped tokens |
| The same artifact accessible from Workers, HTTP APIs, and developer tooling | Artifacts | Artifacts is available through a **Workers binding**, **REST API**, and **git-compatible interface** |
| Large files by object key, app config by key, or relational app data | R2, KV, or D1 | Use storage products directly when you need **objects, key-value entries, or SQL rows**, not versioned file trees |
## Recommended Workflow
- Create one repo per agent, session, user workspace, or task when work should stay isolated.
- Fork from a stable baseline when many repos need the same starter files or prompts.
- Use branches only when collaborators share the same lifecycle and need to work in one repo.
- Use namespaces to separate environments, teams, or high-rate workloads.
## Quick Start
**From a Worker:**
```typescript
interface Env {
ARTIFACTS: Artifacts;
}
const created = await env.ARTIFACTS.create("starter-repo");
// created.remote -> git remote URL
// created.token -> initial repo token
```
**From the REST API:**
Use the namespace-scoped Artifacts base URL plus a gateway JWT. For imports from existing HTTPS remotes, use the REST API rather than the Workers binding.
## Reading Order
| Task | Read |
|------|------|
| Decide whether Artifacts is the right product | README only |
| Create or manage repos from a Worker | README → configuration.md → api.md |
| Integrate Artifacts from an external system | README → api.md |
| Set up agent or sandbox workflows | README → configuration.md |
| Verify exact auth, routes, limits, or pricing | Live docs first: `https://developers.cloudflare.com/artifacts/` |
## In This Reference
- **[api.md](api.md)** - Workers binding methods, REST routes, token and repo operations
- **[configuration.md](configuration.md)** - Wrangler binding shape, Worker typing, REST configuration guidance
## See Also
- [Cloudflare Artifacts Docs](https://developers.cloudflare.com/artifacts/)
- [Artifacts Git Protocol Docs](https://developers.cloudflare.com/artifacts/api/git-protocol/)
- [ArtifactFS Docs](https://developers.cloudflare.com/artifacts/api/artifactfs/)
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
- [Cloudflare Durable Objects Docs](https://developers.cloudflare.com/durable-objects/)
- [Cloudflare R2 Docs](https://developers.cloudflare.com/r2/)
- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1/)
@@ -0,0 +1,128 @@
# Artifacts API Reference
Use Artifacts through the **Workers binding**, the **REST control plane**, and **Git-compatible remotes**.
**Prefer retrieval** for exact request and response details. Verify current behavior at `https://developers.cloudflare.com/artifacts/` before relying on specific auth flows, route details, or generated binding types.
## Workers Binding
Artifacts exposes a Worker binding on `env.ARTIFACTS`.
### Namespace Methods
| Method | Use For |
|--------|---------|
| `create(name, opts?)` | Create a repo and receive its initial remote and token |
| `get(name)` | Resolve a repo handle for repo-scoped operations |
| `list(opts?)` | List repos in a namespace |
| `delete(name)` | Delete a repo |
```typescript
const created = await env.ARTIFACTS.create("starter-repo", {
description: "Repository for automation experiments",
setDefaultBranch: "main"
});
const repo = await env.ARTIFACTS.get("starter-repo");
const page = await env.ARTIFACTS.list({ limit: 10 });
```
Use the REST API when you need to import a repo from another HTTPS remote.
### Repo Handle Methods
Use a repo handle returned by `get()` or `create()`.
| Method | Use For |
|--------|---------|
| `info()` | Read repo metadata, including the remote URL |
| `createToken(scope?, ttl?)` | Mint a repo-scoped read or write token |
| `listTokens()` | Inspect active tokens |
| `validateToken(token)` | Check whether a token is still valid |
| `revokeToken(tokenOrId)` | Revoke a token by ID or value |
| `fork(name, opts?)` | Fork one repo into another |
```typescript
const repo = await env.ARTIFACTS.get("starter-repo");
if (!repo) throw new Error("Repo not found");
const info = await repo.info();
const token = await repo.createToken("read", 3600);
const forked = await repo.fork("starter-repo-copy", {
defaultBranchOnly: true
});
```
### Binding Notes
- Current docs describe the runtime binding surface as `create`, `get`, `list`, `delete`, and repo-handle methods like `info`, `createToken`, and `fork`.
- Use `npx wrangler types` in the target project and treat the generated `worker-configuration.d.ts` as the source of truth for that environment.
- If generated types appear to expose `import()` or a different `get()` shape, verify the live docs before depending on those methods.
Verify current runtime behavior in the live docs before depending on methods that are not shown in the Workers binding reference.
## REST API
Artifacts currently documents a namespace-scoped control plane:
```txt
https://artifacts.cloudflare.net/v1/api/namespaces/$ARTIFACTS_NAMESPACE
```
Some deployments also expose an `/edge/v1/api/...` base path. Verify the correct base URL for your environment in the live docs.
Requests to the standard `/v1/api/...` routes use a **gateway JWT** with Bearer authentication.
Returned repo tokens authenticate **Git operations** against the repo `remote`. They do not authenticate REST control-plane requests.
Current docs show the standard Cloudflare v4 response envelope around REST results.
### Repo Routes
| Route | Use For |
|-------|---------|
| `POST /repos` | Create a repo |
| `GET /repos` | List repos |
| `GET /repos/:name` | Read repo metadata and remote |
| `DELETE /repos/:name` | Delete a repo |
| `POST /repos/:name/fork` | Fork a repo |
| `POST /repos/:name/import` | Import a public HTTPS remote |
```bash
curl --request POST "$ARTIFACTS_BASE_URL/repos" \
--header "Authorization: Bearer $ARTIFACTS_JWT" \
--header "Content-Type: application/json" \
--data '{"name":"starter-repo"}'
```
Important current details from the docs draft:
- `POST /repos/:name/import` accepts a full HTTPS remote URL such as GitHub or GitLab.
- Import supports options such as `branch`, `depth`, and `read_only`.
- Repo metadata includes fields such as description, default branch, timestamps, and the Git `remote`.
### Token Routes
| Route | Use For |
|-------|---------|
| `GET /repos/:name/tokens` | List repo tokens |
| `POST /tokens` | Create a token for a repo |
| `DELETE /tokens/:id` | Revoke a token by ID |
Current docs show list-token filtering and pagination by token state. Retrieve the exact query shape from the live docs when you need token audit or cleanup workflows.
Use **read** tokens for clone, fetch, pull, and indexing workflows. Use **write** tokens only when a workflow must push or otherwise mutate a repo.
## Git-Compatible Access
Artifacts returns repo `remote` URLs that work with standard git-over-HTTPS tooling.
Recommended current auth pattern for local workflows:
```bash
git -c http.extraHeader="Authorization: Bearer $ARTIFACTS_TOKEN" clone "$ARTIFACTS_REMOTE" artifacts-clone
```
Use a self-contained Basic-auth remote only for short-lived commands that need credentials embedded in the URL.
`read` tokens support `clone`, `fetch`, and `pull`. `git push` requires a `write` token.
For large repos where startup time matters more than a full clone, Artifacts also documents **ArtifactFS**. Retrieve current details from `https://developers.cloudflare.com/artifacts/` when you need mount-style access.
@@ -0,0 +1,92 @@
# Artifacts Configuration
## Worker Binding
Configure the `artifacts` binding in your Wrangler config:
```toml
[[artifacts]]
binding = "ARTIFACTS"
namespace = "default"
```
This exposes Artifacts on `env.ARTIFACTS` inside your Worker.
If you authenticate with `wrangler login`, current docs say Wrangler requests `artifacts:write` by default.
## TypeScript
Regenerate Worker types after adding the binding:
```bash
npx wrangler types
```
Use the generated binding type in your environment definition:
```typescript
interface Env {
ARTIFACTS: Artifacts;
}
```
Wrangler generates the `Artifacts` type from the binding. Treat the generated `worker-configuration.d.ts` file as the source of truth for your environment.
## Structure Repos for Isolation
Artifacts works best when autonomous work is isolated:
- Create one repo per agent, session, sandbox, or task when work should stay separate.
- Fork from a reviewed baseline instead of copying starter files into every new repo.
- Use branches only when collaborators share the same lifecycle and need to work in one repo.
- Use namespaces to separate environments, teams, or high-rate workloads.
## REST Configuration
For external systems, configure the namespace-scoped base URL and gateway JWT:
```bash
export ARTIFACTS_NAMESPACE="default"
export ARTIFACTS_JWT="<YOUR_GATEWAY_JWT>"
export ARTIFACTS_BASE_URL="https://artifacts.cloudflare.net/v1/api/namespaces/$ARTIFACTS_NAMESPACE"
```
Some environments also expose an `/edge/v1/api/...` base path. Verify the correct host and base path in the live docs for your Artifacts environment.
Use environment variables or your secret manager. Do not hardcode gateway JWTs or repo tokens.
## Repo Tokens
Artifacts workflows usually involve repo-scoped tokens returned by `create()` or minted later through the binding or REST API.
Keep the control plane and data plane separate:
- Use the **Workers binding** or **REST API** with a gateway JWT to create repos and mint tokens.
- Use repo-scoped tokens only for **Git operations** against the returned `remote`.
Recommended handling:
- Mint the narrowest scope you need: `read` or `write`
- Prefer short-lived tokens for handoff between systems
- Revoke tokens that are no longer needed
Verify the current token behavior and auth guidance in `https://developers.cloudflare.com/artifacts/` before building long-lived automation.
## Git Consumers
Artifacts is designed to work with standard git-over-HTTPS clients once you have a repo `remote` and an access token.
Prefer header-based auth for local tooling so the full token stays out of the remote URL:
```bash
git -c http.extraHeader="Authorization: Bearer $ARTIFACTS_TOKEN" clone "$ARTIFACTS_REMOTE" artifacts-clone
```
Use a Basic-auth remote only for short-lived commands that need a self-contained URL.
## Retrieval Checklist
Check the live docs before relying on:
- the current Workers binding surface
- exact token formats
- availability or product status
- route details for import, fork, and token-management flows
- the correct control-plane host or `/edge/v1` base path for your environment
- platform limits or pricing
@@ -0,0 +1,122 @@
# Cloudflare Bindings Skill Reference
Expert guidance on Cloudflare Workers Bindings - the runtime APIs that connect Workers to Cloudflare platform resources.
## What Are Bindings?
Bindings are how Workers access Cloudflare resources (storage, compute, services) via the `env` object. They're configured in `wrangler.jsonc`, type-safe via TypeScript, and zero-overhead at runtime.
## Reading Order
1. **This file** - Binding catalog and selection guide
2. **[api.md](api.md)** - TypeScript types and env access patterns
3. **[configuration.md](configuration.md)** - Complete wrangler.jsonc examples
4. **[patterns.md](patterns.md)** - Best practices and common patterns
5. **[gotchas.md](gotchas.md)** - Critical pitfalls and troubleshooting
## Binding Catalog
### Storage Bindings
| Binding | Use Case | Access Pattern |
|---------|----------|----------------|
| **KV** | Key-value cache, CDN-backed reads | `env.MY_KV.get(key)` |
| **R2** | Object storage (S3-compatible) | `env.MY_BUCKET.get(key)` |
| **D1** | SQL database (SQLite) | `env.DB.prepare(sql).all()` |
| **Durable Objects** | Coordination, real-time state | `env.MY_DO.get(id)` |
| **Vectorize** | Vector embeddings search | `env.VECTORIZE.query(vector)` |
| **Queues** | Async message processing | `env.MY_QUEUE.send(msg)` |
### Compute Bindings
| Binding | Use Case | Access Pattern |
|---------|----------|----------------|
| **Service** | Worker-to-Worker RPC | `env.MY_SERVICE.fetch(req)` |
| **Workers AI** | LLM inference | `env.AI.run(model, input)` |
| **Browser Rendering** | Headless Chrome | `env.BROWSER.fetch(url)` |
### Platform Bindings
| Binding | Use Case | Access Pattern |
|---------|----------|----------------|
| **Analytics Engine** | Custom metrics | `env.ANALYTICS.writeDataPoint(data)` |
| **mTLS** | Client certificates | `env.MY_CERT` (string) |
| **Hyperdrive** | Database pooling | `env.HYPERDRIVE.connectionString` |
| **Rate Limiting** | Request throttling | `env.RATE_LIMITER.limit(id)` |
| **Workflows** | Long-running workflows | `env.MY_WORKFLOW.create()` |
### Configuration Bindings
| Binding | Use Case | Access Pattern |
|---------|----------|----------------|
| **Environment Variables** | Non-sensitive config | `env.API_URL` (string) |
| **Secrets** | Sensitive values | `env.API_KEY` (string) |
| **Text/Data Blobs** | Static files | `env.MY_BLOB` (string) |
| **WASM** | WebAssembly modules | `env.MY_WASM` (WebAssembly.Module) |
## Quick Selection Guide
**Need persistent storage?**
- Key-value < 25MB → **KV**
- Files/objects → **R2**
- Relational data → **D1**
- Real-time coordination → **Durable Objects**
**Need AI/compute?**
- LLM inference → **Workers AI**
- Scraping/PDFs → **Browser Rendering**
- Call another Worker → **Service binding**
**Need async processing?**
- Background jobs → **Queues**
**Need config?**
- Public values → **Environment Variables**
- Secrets → **Secrets** (never commit)
## Quick Start
1. **Add binding to wrangler.jsonc:**
```jsonc
{
"kv_namespaces": [
{ "binding": "MY_KV", "id": "your-kv-id" }
]
}
```
2. **Generate types:**
```bash
npx wrangler types
```
3. **Access in Worker:**
```typescript
export default {
async fetch(request, env, ctx) {
await env.MY_KV.put('key', 'value');
return new Response('OK');
}
}
```
## Type Safety
Bindings are fully typed via `wrangler types`. See [api.md](api.md) for details.
## Limits
- 64 bindings max per Worker (all types combined)
- See [gotchas.md](gotchas.md) for per-binding limits
## Key Concepts
**Zero-overhead access:** Bindings compiled into Worker, no network calls to access
**Type-safe:** Full TypeScript support via `wrangler types`
**Per-environment:** Different IDs for dev/staging/production
**Secrets vs Vars:** Secrets encrypted at rest, never in config files
## See Also
- [Cloudflare Docs: Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)
- [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)
@@ -0,0 +1,203 @@
# Bindings API Reference
## TypeScript Types
Cloudflare generates binding types via `npx wrangler types`. This creates `.wrangler/types/runtime.d.ts` with your Env interface.
### Generated Env Interface
After running `wrangler types`, TypeScript knows your bindings:
```typescript
interface Env {
// From wrangler.jsonc bindings
MY_KV: KVNamespace;
MY_BUCKET: R2Bucket;
DB: D1Database;
MY_SERVICE: Fetcher;
AI: Ai;
// From vars
API_URL: string;
// From secrets (set via wrangler secret put)
API_KEY: string;
}
```
### Binding Types
| Config | TypeScript Type | Package |
|--------|-----------------|---------|
| `kv_namespaces` | `KVNamespace` | `@cloudflare/workers-types` |
| `r2_buckets` | `R2Bucket` | `@cloudflare/workers-types` |
| `d1_databases` | `D1Database` | `@cloudflare/workers-types` |
| `durable_objects.bindings` | `DurableObjectNamespace` | `@cloudflare/workers-types` |
| `vectorize` | `VectorizeIndex` | `@cloudflare/workers-types` |
| `queues.producers` | `Queue` | `@cloudflare/workers-types` |
| `services` | `Fetcher` | `@cloudflare/workers-types` |
| `ai` | `Ai` | `@cloudflare/workers-types` |
| `browser` | `Fetcher` | `@cloudflare/workers-types` |
| `analytics_engine_datasets` | `AnalyticsEngineDataset` | `@cloudflare/workers-types` |
| `hyperdrive` | `Hyperdrive` | `@cloudflare/workers-types` |
| `rate_limiting` | `RateLimit` | `@cloudflare/workers-types` |
| `workflows` | `Workflow` | `@cloudflare/workers-types` |
| `mtls_certificates` / `vars` / `text_blobs` / `data_blobs` | `string` | Built-in |
| `wasm_modules` | `WebAssembly.Module` | Built-in |
## Accessing Bindings
### Method 1: fetch() Handler (Recommended)
```typescript
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const value = await env.MY_KV.get('key');
return new Response(value);
}
}
```
**Why:** Type-safe, aligns with Workers API, supports ctx for waitUntil/passThroughOnException.
### Method 2: Hono Framework
```typescript
import { Hono } from 'hono';
const app = new Hono<{ Bindings: Env }>();
app.get('/', async (c) => {
const value = await c.env.MY_KV.get('key');
return c.json({ value });
});
export default app;
```
**Why:** c.env auto-typed, ergonomic for routing-heavy apps.
### Method 3: Module Workers (Legacy)
```typescript
export async function handleRequest(request: Request, env: Env): Promise<Response> {
const value = await env.MY_KV.get('key');
return new Response(value);
}
addEventListener('fetch', (event) => {
// env not directly available - requires workarounds
});
```
**Avoid:** Use fetch() handler instead (Method 1).
## Type Generation Workflow
### Initial Setup
```bash
# Install wrangler
npm install -D wrangler
# Generate types from wrangler.jsonc
npx wrangler types
```
### After Changing Bindings
```bash
# Added/modified binding in wrangler.jsonc
npx wrangler types
# TypeScript now sees updated Env interface
```
**Note:** `wrangler types` outputs to `.wrangler/types/runtime.d.ts`. TypeScript picks this up automatically if `@cloudflare/workers-types` is in `tsconfig.json` `"types"` array.
## Key Binding Methods
**KV:**
```typescript
await env.MY_KV.get(key, { type: 'json' }); // text|json|arrayBuffer|stream
await env.MY_KV.put(key, value, { expirationTtl: 3600 });
await env.MY_KV.delete(key);
await env.MY_KV.list({ prefix: 'user:' });
```
**R2:**
```typescript
await env.BUCKET.get(key);
await env.BUCKET.put(key, value);
await env.BUCKET.delete(key);
await env.BUCKET.list({ prefix: 'images/' });
```
**D1:**
```typescript
await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
await env.DB.batch([stmt1, stmt2]);
```
**Service:**
```typescript
await env.MY_SERVICE.fetch(new Request('https://fake/path'));
```
**Workers AI:**
```typescript
await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' });
```
**Queues:**
```typescript
await env.MY_QUEUE.send({ userId: 123, action: 'process' });
```
**Durable Objects:**
```typescript
const id = env.MY_DO.idFromName('user-123');
const stub = env.MY_DO.get(id);
await stub.fetch(new Request('https://fake/increment'));
```
## Runtime vs Build-Time Types
| Type Source | When Generated | Use Case |
|-------------|----------------|----------|
| `@cloudflare/workers-types` | npm install | Base Workers APIs (Request, Response, etc.) |
| `wrangler types` | After config change | Your specific bindings (Env interface) |
**Install both:**
```bash
npm install -D @cloudflare/workers-types
npx wrangler types
```
## Type Safety Best Practices
1. **Never use `any` for env:**
```typescript
// ❌ BAD
async fetch(request: Request, env: any) { }
// ✅ GOOD
async fetch(request: Request, env: Env) { }
```
2. **Run wrangler types after config changes:**
```bash
# After editing wrangler.jsonc
npx wrangler types
```
3. **Check generated types match config:**
```bash
# View generated Env interface
cat .wrangler/types/runtime.d.ts
```
## See Also
- [Workers Types Package](https://www.npmjs.com/package/@cloudflare/workers-types)
- [Wrangler Types Command](https://developers.cloudflare.com/workers/wrangler/commands/#types)
@@ -0,0 +1,188 @@
# Binding Configuration Reference
## Storage Bindings
```jsonc
{
"kv_namespaces": [{ "binding": "MY_KV", "id": "..." }],
"r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }],
"d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "..." }],
"durable_objects": { "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] },
"vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }],
"queues": { "producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }] }
}
```
**Create commands:**
```bash
npx wrangler kv namespace create MY_KV
npx wrangler r2 bucket create my-bucket
npx wrangler d1 create my-db
npx wrangler vectorize create my-index --dimensions=768 --metric=cosine
npx wrangler queues create my-queue
# List existing resources
npx wrangler kv namespace list
npx wrangler r2 bucket list
npx wrangler d1 list
npx wrangler vectorize list
npx wrangler queues list
```
## Compute Bindings
```jsonc
{
"services": [{
"binding": "MY_SERVICE",
"service": "other-worker",
"environment": "production" // Optional: target specific env
}],
"ai": { "binding": "AI" },
"browser": { "binding": "BROWSER" },
"workflows": [{ "binding": "MY_WORKFLOW", "name": "my-workflow" }]
}
```
**Create workflows:**
```bash
npx wrangler workflows create my-workflow
```
## Platform Bindings
```jsonc
{
"analytics_engine_datasets": [{ "binding": "ANALYTICS" }],
"mtls_certificates": [{ "binding": "MY_CERT", "certificate_id": "..." }],
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "..." }],
"unsafe": {
"bindings": [{ "name": "RATE_LIMITER", "type": "ratelimit", "namespace_id": "..." }]
}
}
```
## Configuration Bindings
```jsonc
{
"vars": {
"API_URL": "https://api.example.com",
"MAX_RETRIES": "3"
},
"text_blobs": { "MY_TEXT": "./data/template.html" },
"data_blobs": { "MY_DATA": "./data/config.bin" },
"wasm_modules": { "MY_WASM": "./build/module.wasm" }
}
```
**Secrets (never in config):**
```bash
npx wrangler secret put API_KEY
```
## Environment-Specific Configuration
```jsonc
{
"name": "my-worker",
"vars": { "ENV": "production" },
"kv_namespaces": [{ "binding": "CACHE", "id": "prod-kv-id" }],
"env": {
"staging": {
"vars": { "ENV": "staging" },
"kv_namespaces": [{ "binding": "CACHE", "id": "staging-kv-id" }]
}
}
}
```
**Deploy:**
```bash
npx wrangler deploy # Production
npx wrangler deploy --env staging
```
## Local Development
```jsonc
{
"kv_namespaces": [{
"binding": "MY_KV",
"id": "prod-id",
"preview_id": "dev-id" // Used in wrangler dev
}]
}
```
**Or use remote:**
```bash
npx wrangler dev --remote # Uses production bindings
```
## Complete Example
```jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"vars": { "API_URL": "https://api.example.com" },
"kv_namespaces": [{ "binding": "CACHE", "id": "abc123" }],
"r2_buckets": [{ "binding": "ASSETS", "bucket_name": "my-assets" }],
"d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }],
"services": [{ "binding": "AUTH", "service": "auth-worker" }],
"ai": { "binding": "AI" }
}
```
## Binding-Specific Configuration
### Durable Objects with Class Export
```jsonc
{
"durable_objects": {
"bindings": [
{ "name": "COUNTER", "class_name": "Counter", "script_name": "my-worker" }
]
}
}
```
```typescript
// In same Worker or script_name Worker
export class Counter {
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request) { /* ... */ }
}
```
### Queue Consumers
```jsonc
{
"queues": {
"producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }],
"consumers": [{ "queue": "my-queue", "max_batch_size": 10 }]
}
}
```
Queue consumer handler: `export default { async queue(batch, env) { /* process batch.messages */ } }`
## Key Points
- **64 binding limit** (all types combined)
- **Secrets**: Always use `wrangler secret put`, never commit
- **Types**: Run `npx wrangler types` after config changes
- **Environments**: Use `env` field for staging/production variants
- **Development**: Use `preview_id` or `--remote` flag
- **IDs vs Names**: Some bindings use `id` (KV, D1), others use `name` (R2, Queues)
## See Also
- [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)
@@ -0,0 +1,208 @@
# Binding Gotchas and Troubleshooting
## Critical: Global Scope Mutation
### ❌ THE #1 GOTCHA: Caching env in Global Scope
```typescript
// ❌ DANGEROUS - env cached at deploy time
const apiKey = env.API_KEY; // ERROR: env not available in global scope
export default {
async fetch(request: Request, env: Env) {
// Uses undefined or stale value!
}
}
```
**Why it breaks:**
- `env` not available in global scope
- If using workarounds, secrets may not update without redeployment
- Leads to "Cannot read property 'X' of undefined" errors
**✅ Always access env per-request:**
```typescript
export default {
async fetch(request: Request, env: Env) {
const apiKey = env.API_KEY; // Fresh every request
}
}
```
## Common Errors
### "env.MY_KV is undefined"
**Cause:** Name mismatch or not configured
**Solution:** Check wrangler.jsonc (case-sensitive), run `npx wrangler types`, verify `npx wrangler kv namespace list`
### "Property 'MY_KV' does not exist on type 'Env'"
**Cause:** Types not generated
**Solution:** `npx wrangler types`
### "preview_id is required for --remote"
**Cause:** Missing preview binding
**Solution:** Add `"preview_id": "dev-id"` or use `npx wrangler dev` (local mode)
### "Secret updated but Worker still uses old value"
**Cause:** Cached in global scope or not redeployed
**Solution:** Avoid global caching, redeploy after secret change
### "KV get() returns null for existing key"
**Cause:** Eventual consistency (60s), wrong namespace, wrong environment
**Solution:**
```bash
# Check key exists
npx wrangler kv key get --binding=MY_KV "your-key"
# Verify namespace ID
npx wrangler kv namespace list
# Check environment
npx wrangler deployments list
```
### "D1 database not found"
**Solution:** `npx wrangler d1 list`, verify ID in wrangler.jsonc
### "Service binding returns 'No such service'"
**Cause:** Target Worker not deployed, name mismatch, environment mismatch
**Solution:**
```bash
# List deployed Workers
npx wrangler deployments list --name=target-worker
# Check service binding config
cat wrangler.jsonc | grep -A2 services
# Deploy target first
cd ../target-worker && npx wrangler deploy
```
### "Rate limit exceeded" on KV writes
**Cause:** >1 write/second per key
**Solution:** Use different keys, Durable Objects, or Queues
## Type Safety Gotchas
### Missing @cloudflare/workers-types
**Error:** `Cannot find name 'Request'`
**Solution:** `npm install -D @cloudflare/workers-types`, add to tsconfig.json `"types"`
### Binding Type Mismatches
```typescript
// ❌ Wrong - KV returns string | null
const value: string = await env.MY_KV.get('key');
// ✅ Handle null
const value = await env.MY_KV.get('key');
if (!value) return new Response('Not found', { status: 404 });
```
## Environment Gotchas
### Wrong Environment Deployed
**Solution:** Check `npx wrangler deployments list`, use `--env` flag
### Secrets Not Per-Environment
**Solution:** Set per environment: `npx wrangler secret put API_KEY --env staging`
## Development Gotchas
**wrangler dev vs deploy:**
- dev: Uses `preview_id` or local bindings, secrets not available
- deploy: Uses production `id`, secrets available
**Access secrets in dev:** `npx wrangler dev --remote`
**Persist local data:** `npx wrangler dev --persist`
## Performance Gotchas
### Sequential Binding Calls
```typescript
// ❌ Slow
const user = await env.DB.prepare('...').first();
const config = await env.MY_KV.get('config');
// ✅ Parallel
const [user, config] = await Promise.all([
env.DB.prepare('...').first(),
env.MY_KV.get('config')
]);
```
## Security Gotchas
**❌ Secrets in logs:** `console.log('Key:', env.API_KEY)` - visible in dashboard
**✅** `console.log('Key:', env.API_KEY ? '***' : 'missing')`
**❌ Exposing env:** `return Response.json(env)` - exposes all bindings
**✅** Never return env object in responses
## Limits Reference
| Resource | Limit | Impact | Plan |
|----------|-------|--------|------|
| **Bindings per Worker** | 64 total | All binding types combined | All |
| **Environment variables** | 64 max, 5KB each | Per Worker | All |
| **Secret size** | 1KB | Per secret | All |
| **KV key size** | 512 bytes | UTF-8 encoded | All |
| **KV value size** | 25 MB | Per value | All |
| **KV writes per key** | 1/second | Per key; exceeding = 429 error | All |
| **KV list() results** | 1000 keys | Per call; use cursor for more | All |
| **KV operations** | 1000 reads/day | Free tier only | Free |
| **R2 object size** | 5 TB | Per object | All |
| **R2 operations** | 1M Class A/month free | Writes | All |
| **D1 database size** | 10 GB | Per database | All |
| **D1 rows per query** | 100,000 | Result set limit | All |
| **D1 databases** | 10 | Free tier | Free |
| **Queue batch size** | 100 messages | Per consumer batch | All |
| **Queue message size** | 128 KB | Per message | All |
| **Service binding calls** | Unlimited | Counts toward CPU time | All |
| **Durable Objects** | 1M requests/month free | First 1M | Free |
## Debugging Tips
```bash
# Check configuration
npx wrangler deploy --dry-run # Validate config without deploying
npx wrangler kv namespace list # List KV namespaces
npx wrangler secret list # List secrets (not values)
npx wrangler deployments list # Recent deployments
# Inspect bindings
npx wrangler kv key list --binding=MY_KV
npx wrangler kv key get --binding=MY_KV "key-name"
npx wrangler r2 object get my-bucket/file.txt
npx wrangler d1 execute my-db --command="SELECT * FROM sqlite_master"
# Test locally
npx wrangler dev # Local mode
npx wrangler dev --remote # Production bindings
npx wrangler dev --persist # Persist data across restarts
# Verify types
npx wrangler types
cat .wrangler/types/runtime.d.ts | grep "interface Env"
# Debug specific binding issues
npx wrangler tail # Stream logs in real-time
npx wrangler tail --format=pretty # Formatted logs
```
## See Also
- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)
- [Wrangler Commands](https://developers.cloudflare.com/workers/wrangler/commands/)
@@ -0,0 +1,200 @@
# Binding Patterns and Best Practices
## Service Binding Patterns
### RPC via Service Bindings
```typescript
// auth-worker
export default {
async fetch(request: Request, env: Env) {
const token = request.headers.get('Authorization');
return new Response(JSON.stringify({ valid: await validateToken(token) }));
}
}
// api-worker
const response = await env.AUTH_SERVICE.fetch(
new Request('https://fake-host/validate', {
headers: { 'Authorization': token }
})
);
```
**Why RPC?** Zero latency (same datacenter), no DNS, free, type-safe.
**HTTP vs Service:**
```typescript
// ❌ HTTP (slow, paid, cross-region latency)
await fetch('https://auth-worker.example.com/validate');
// ✅ Service binding (fast, free, same isolate)
await env.AUTH_SERVICE.fetch(new Request('https://fake-host/validate'));
```
**URL doesn't matter:** Service bindings ignore hostname/protocol, routing happens via binding name.
### Typed Service RPC
```typescript
// shared-types.ts
export interface AuthRequest { token: string; }
export interface AuthResponse { valid: boolean; userId?: string; }
// auth-worker
export default {
async fetch(request: Request): Promise<Response> {
const body: AuthRequest = await request.json();
const response: AuthResponse = { valid: true, userId: '123' };
return Response.json(response);
}
}
// api-worker
const response = await env.AUTH_SERVICE.fetch(
new Request('https://fake/validate', {
method: 'POST',
body: JSON.stringify({ token } satisfies AuthRequest)
})
);
const data: AuthResponse = await response.json();
```
## Secrets Management
```bash
# Set secret
npx wrangler secret put API_KEY
cat api-key.txt | npx wrangler secret put API_KEY
npx wrangler secret put API_KEY --env staging
```
```typescript
// Use secret
const response = await fetch('https://api.example.com', {
headers: { 'Authorization': `Bearer ${env.API_KEY}` }
});
```
**Never commit secrets:**
```jsonc
// ❌ NEVER
{ "vars": { "API_KEY": "sk_live_abc123" } }
```
## Testing with Mock Bindings
### Vitest Mock
```typescript
import { vi } from 'vitest';
const mockKV: KVNamespace = {
get: vi.fn(async (key) => key === 'test' ? 'value' : null),
put: vi.fn(async () => {}),
delete: vi.fn(async () => {}),
list: vi.fn(async () => ({ keys: [], list_complete: true, cursor: '' })),
getWithMetadata: vi.fn(),
} as unknown as KVNamespace;
const mockEnv: Env = { MY_KV: mockKV };
const mockCtx: ExecutionContext = {
waitUntil: vi.fn(),
passThroughOnException: vi.fn(),
};
const response = await worker.fetch(
new Request('http://localhost/test'),
mockEnv,
mockCtx
);
```
## Binding Access Patterns
### Lazy Access
```typescript
// ✅ Access only when needed
if (url.pathname === '/cached') {
const cached = await env.MY_KV.get('data');
if (cached) return new Response(cached);
}
```
### Parallel Access
```typescript
// ✅ Parallelize independent calls
const [user, config, cache] = await Promise.all([
env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(),
env.MY_KV.get('config'),
env.CACHE.get('data')
]);
```
## Storage Selection
### KV: CDN-Backed Reads
```typescript
const config = await env.MY_KV.get('app-config', { type: 'json' });
```
**Use when:** Read-heavy, <25MB, global distribution, eventual consistency OK
**Latency:** <10ms reads (cached), writes eventually consistent (60s)
### D1: Relational Queries
```typescript
const results = await env.DB.prepare(`
SELECT u.name, COUNT(o.id) FROM users u
LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id
`).all();
```
**Use when:** Relational data, JOINs, ACID transactions
**Limits:** 10GB database size, 100k rows per query
### R2: Large Objects
```typescript
const object = await env.MY_BUCKET.get('large-file.zip');
return new Response(object.body);
```
**Use when:** Files >25MB, S3-compatible API needed
**Limits:** 5TB per object, unlimited storage
### Durable Objects: Coordination
```typescript
const id = env.COUNTER.idFromName('global');
const stub = env.COUNTER.get(id);
await stub.fetch(new Request('https://fake/increment'));
```
**Use when:** Strong consistency, real-time coordination, WebSocket state
**Guarantees:** Single-threaded execution, transactional storage
## Anti-Patterns
**❌ Hardcoding credentials:** `const apiKey = 'sk_live_abc123'`
**✅** `npx wrangler secret put API_KEY`
**❌ Using REST API:** `fetch('https://api.cloudflare.com/.../kv/...')`
**✅** `env.MY_KV.get('key')`
**❌ Polling storage:** `setInterval(() => env.KV.get('config'), 1000)`
**✅** Use Durable Objects for real-time state
**❌ Large data in vars:** `{ "vars": { "HUGE_CONFIG": "..." } }` (5KB max)
**✅** `env.MY_KV.put('config', data)`
**❌ Caching env globally:** `const apiKey = env.API_KEY` outside fetch()
**✅** Access `env.API_KEY` per-request inside fetch()
## See Also
- [Service Bindings Docs](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/)
- [Miniflare Testing](https://miniflare.dev/)
@@ -0,0 +1,94 @@
# Cloudflare Bot Management
Enterprise-grade bot detection, protection, and mitigation using ML/heuristics, bot scores, JavaScript detections, and verified bot handling.
## Overview
Bot Management provides multi-tier protection:
- **Free (Bot Fight Mode)**: Auto-blocks definite bots, no config
- **Pro/Business (Super Bot Fight Mode)**: Configurable actions, static resource protection, analytics groupings
- **Enterprise (Bot Management)**: Granular 1-99 scores, WAF integration, JA3/JA4 fingerprinting, Workers API, Advanced Analytics
## Quick Start
```txt
# Dashboard: Security > Bots
# Enterprise: Deploy rule template
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot) → Block
(cf.bot_management.score le 29 and not cf.bot_management.verified_bot) → Managed Challenge
```
## What Do You Need?
```txt
├─ Initial setup → configuration.md
│ ├─ Free tier → "Bot Fight Mode"
│ ├─ Pro/Business → "Super Bot Fight Mode"
│ └─ Enterprise → "Bot Management for Enterprise"
├─ Workers API integration → api.md
├─ WAF rules → patterns.md
├─ Debugging → gotchas.md
└─ Analytics → api.md#bot-analytics
```
## Reading Order
| Task | Files to Read |
|------|---------------|
| Enable bot protection | README → configuration.md |
| Workers bot detection | README → api.md |
| WAF rule templates | README → patterns.md |
| Debug bot issues | gotchas.md |
| Advanced analytics | api.md#bot-analytics |
## Core Concepts
**Bot Scores**: 1-99 (1 = definitely automated, 99 = definitely human). Threshold: <30 indicates bot traffic. Enterprise gets granular 1-99; Pro/Business get groupings only.
**Detection Engines**: Heuristics (known fingerprints, assigns score=1), ML (majority of detections, supervised learning on billions of requests), Anomaly Detection (optional, baseline traffic analysis), JavaScript Detections (headless browser detection).
**Verified Bots**: Allowlisted good bots (search engines, AI crawlers) verified via reverse DNS or Web Bot Auth. Access via `cf.bot_management.verified_bot` or `cf.verified_bot_category`.
## Platform Limits
| Plan | Bot Scores | JA3/JA4 | Custom Rules | Analytics Retention |
|------|------------|---------|--------------|---------------------|
| Free | No (auto-block only) | No | 5 | N/A (no analytics) |
| Pro/Business | Groupings only | No | 20/100 | 30 days (72h at a time) |
| Enterprise | 1-99 granular | Yes | 1,000+ | 30 days (1 week at a time) |
## Basic Patterns
```typescript
// Workers: Check bot score
export default {
async fetch(request: Request): Promise<Response> {
const botScore = request.cf?.botManagement?.score;
if (botScore && botScore < 30 && !request.cf?.botManagement?.verifiedBot) {
return new Response('Bot detected', { status: 403 });
}
return fetch(request);
}
};
```
```txt
# WAF: Block definite bots
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot)
# WAF: Protect sensitive endpoints
(cf.bot_management.score lt 50 and http.request.uri.path in {"/login" "/checkout"} and not cf.bot_management.verified_bot)
```
## In This Reference
- [configuration.md](./configuration.md) - Product tiers, WAF rule setup, JavaScript Detections, ML auto-updates
- [api.md](./api.md) - Workers BotManagement interface, WAF fields, JA4 Signals
- [patterns.md](./patterns.md) - E-commerce, API protection, mobile app allowlisting, SEO-friendly handling
- [gotchas.md](./gotchas.md) - False positives/negatives, score=0 issues, JSD limitations, CSP requirements
## See Also
- [waf](../waf/) - WAF custom rules for bot enforcement
- [workers](../workers/) - Workers request.cf.botManagement API
- [api-shield](../api-shield/) - API-specific bot protection
@@ -0,0 +1,169 @@
# Bot Management API
## Workers: BotManagement Interface
```typescript
interface BotManagement {
score: number; // 1-99 (Enterprise), 0 if not computed
verifiedBot: boolean; // Is verified bot
staticResource: boolean; // Serves static resource
ja3Hash: string; // JA3 fingerprint (Enterprise, HTTPS only)
ja4: string; // JA4 fingerprint (Enterprise, HTTPS only)
jsDetection?: {
passed: boolean; // Passed JS detection (if enabled)
};
detectionIds: number[]; // Heuristic detection IDs
corporateProxy?: boolean; // From corporate proxy (Enterprise)
}
// DEPRECATED: Use botManagement.score instead
// request.cf.clientTrustScore (legacy, duplicate of botManagement.score)
// Access via request.cf
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const botMgmt = cf?.botManagement;
if (!botMgmt) return fetch(request);
if (botMgmt.verifiedBot) return fetch(request); // Allow verified bots
if (botMgmt.score === 1) return new Response('Blocked', { status: 403 });
if (botMgmt.score < 30) return new Response('Challenge required', { status: 429 });
return fetch(request);
}
};
```
## WAF Fields Reference
```txt
# Score fields
cf.bot_management.score # 0-99 (0 = not computed)
cf.bot_management.verified_bot # boolean
cf.bot_management.static_resource # boolean
cf.bot_management.ja3_hash # string (Enterprise)
cf.bot_management.ja4 # string (Enterprise)
cf.bot_management.detection_ids # array
cf.bot_management.js_detection.passed # boolean
cf.bot_management.corporate_proxy # boolean (Enterprise)
cf.verified_bot_category # string
# Workers equivalent
request.cf.botManagement.score
request.cf.botManagement.verifiedBot
request.cf.botManagement.ja3Hash
request.cf.botManagement.ja4
request.cf.botManagement.jsDetection.passed
request.cf.verifiedBotCategory
```
## JA4 Signals (Enterprise)
```typescript
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
interface JA4Signals {
// Ratios (0.0-1.0)
heuristic_ratio_1h?: number; // Fraction flagged by heuristics
browser_ratio_1h?: number; // Fraction from real browsers
cache_ratio_1h?: number; // Fraction hitting cache
h2h3_ratio_1h?: number; // Fraction using HTTP/2 or HTTP/3
// Ranks (relative position in distribution)
uas_rank_1h?: number; // User-Agent diversity rank
paths_rank_1h?: number; // Path diversity rank
reqs_rank_1h?: number; // Request volume rank
ips_rank_1h?: number; // IP diversity rank
// Quantiles (0.0-1.0, percentile in distribution)
reqs_quantile_1h?: number; // Request volume quantile
ips_quantile_1h?: number; // IP count quantile
}
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const ja4Signals = cf?.ja4Signals as JA4Signals | undefined;
if (!ja4Signals) return fetch(request); // Not available for HTTP or Worker routing
// Check for anomalous behavior
// High heuristic_ratio or low browser_ratio = suspicious
const heuristicRatio = ja4Signals.heuristic_ratio_1h ?? 0;
const browserRatio = ja4Signals.browser_ratio_1h ?? 0;
if (heuristicRatio > 0.5 || browserRatio < 0.3) {
return new Response('Suspicious traffic', { status: 403 });
}
return fetch(request);
}
};
```
## Common Patterns
See [patterns.md](./patterns.md) for Workers examples: mobile app allowlisting, corporate proxy exemption, datacenter detection, conditional delay, and more.
## Bot Analytics
### Access Locations
- Dashboard: Security > Bots (old) or Security > Analytics > Bot analysis (new)
- GraphQL API for programmatic access
- Security Events & Security Analytics
- Logpush/Logpull
### Available Data
- **Enterprise BM**: Bot scores (1-99), bot score source, distribution
- **Pro/Business**: Bot groupings (automated, likely automated, likely human)
- Top attributes: IPs, paths, user agents, countries
- Detection sources: Heuristics, ML, AD, JSD
- Verified bot categories
### Time Ranges
- **Enterprise BM**: Up to 1 week at a time, 30 days history
- **Pro/Business**: Up to 72 hours at a time, 30 days history
- Real-time in most cases, adaptive sampling (1-10% depending on volume)
## Logpush Fields
```txt
BotScore # 1-99 or 0 if not computed
BotScoreSrc # Detection engine (ML, Heuristics, etc.)
BotTags # Classification tags
BotDetectionIDs # Heuristic detection IDs
```
**BotScoreSrc values:**
- `"Heuristics"` - Known fingerprint
- `"Machine Learning"` - ML model
- `"Anomaly Detection"` - Baseline anomaly
- `"JS Detection"` - JavaScript check
- `"Cloudflare Service"` - Zero Trust
- `"Not Computed"` - Score = 0
Access via Logpush (stream to cloud storage/SIEM), Logpull (API to fetch logs), or GraphQL API (query analytics data).
## Testing with Miniflare
Miniflare provides mock botManagement data for local development:
**Default values:**
- `score: 99` (human)
- `verifiedBot: false`
- `corporateProxy: false`
- `ja3Hash: "25b4882c2bcb50cd6b469ff28c596742"`
- `staticResource: false`
- `detectionIds: []`
**Override in tests:**
```typescript
import { getPlatformProxy } from 'wrangler';
const { cf, dispose } = await getPlatformProxy();
// cf.botManagement is frozen mock object
expect(cf.botManagement.score).toBe(99);
```
For custom test data, mock request.cf in your test setup.
@@ -0,0 +1,163 @@
# Bot Management Configuration
## Product Tiers
**Note:** Dashboard paths differ between old and new UI:
- **New:** Security > Settings > Filter "Bot traffic"
- **Old:** Security > Bots
Both UIs access same settings.
### Bot Score Groupings (Pro/Business)
Pro/Business users see bot score groupings instead of granular 1-99 scores:
| Score | Grouping | Meaning |
|-------|----------|---------|
| 0 | Not computed | Bot Management didn't run |
| 1 | Automated | Definite bot (heuristic match) |
| 2-29 | Likely automated | Probably bot (ML detection) |
| 30-99 | Likely human | Probably human |
| N/A | Verified bot | Allowlisted good bot |
Enterprise plans get granular 1-99 scores for custom thresholds.
### Bot Fight Mode (Free)
- Auto-blocks definite bots (score=1), excludes verified bots by default
- JavaScript Detections always enabled, no configuration options
### Super Bot Fight Mode (Pro/Business)
```txt
Dashboard: Security > Bots > Configure
- Definitely automated: Block/Challenge
- Likely automated: Challenge/Allow
- Verified bots: Allow (recommended)
- Static resource protection: ON (may block mail clients)
- JavaScript Detections: Optional
```
### Bot Management for Enterprise
```txt
Dashboard: Security > Bots > Configure > Auto-updates: ON (recommended)
# Template 1: Block definite bots
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)
Action: Block
# Template 2: Challenge likely bots
(cf.bot_management.score ge 2 and cf.bot_management.score le 29 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)
Action: Managed Challenge
```
## JavaScript Detections Setup
### Enable via Dashboard
```txt
Security > Bots > Configure Bot Management > JS Detections: ON
Update CSP: script-src 'self' /cdn-cgi/challenge-platform/;
```
### Manual JS Injection (API)
```html
<script>
function jsdOnload() {
window.cloudflare.jsd.executeOnce({ callback: function(result) { console.log('JSD:', result); } });
}
</script>
<script src="/cdn-cgi/challenge-platform/scripts/jsd/api.js?onload=jsdOnload" async></script>
```
**Use API for**: Selective deployment on specific pages
**Don't combine**: Zone-wide toggle + manual injection
### WAF Rules for JSD
```txt
# NEVER use on first page visit (needs HTML page first)
(not cf.bot_management.js_detection.passed and http.request.uri.path eq "/api/user/create" and http.request.method eq "POST" and not cf.bot_management.verified_bot)
Action: Managed Challenge (always use Managed Challenge, not Block)
```
### Limitations
- First request won't have JSD data (needs HTML page first)
- Strips ETags from HTML responses
- Not supported with CSP via `<meta>` tags
- Websocket endpoints not supported
- Native mobile apps won't pass
- cf_clearance cookie: 15-minute lifespan, max 4096 bytes
## __cf_bm Cookie
Cloudflare sets `__cf_bm` cookie to smooth bot scores across user sessions:
- **Purpose:** Reduces false positives from score volatility
- **Scope:** Per-domain, HTTP-only
- **Lifespan:** Session duration
- **Privacy:** No PII—only session classification
- **Automatic:** No configuration required
Bot scores for repeat visitors consider session history via this cookie.
## Static Resource Protection
**File Extensions**: ico, jpg, png, jpeg, gif, css, js, tif, tiff, bmp, pict, webp, svg, svgz, class, jar, txt, csv, doc, docx, xls, xlsx, pdf, ps, pls, ppt, pptx, ttf, otf, woff, woff2, eot, eps, ejs, swf, torrent, midi, mid, m3u8, m4a, mp3, ogg, ts
**Plus**: `/.well-known/` path (all files)
```txt
# Exclude static resources from bot rules
(cf.bot_management.score lt 30 and not cf.bot_management.static_resource)
```
**WARNING**: May block mail clients fetching static images
## JA3/JA4 Fingerprinting (Enterprise)
```txt
# Block specific attack fingerprint
(cf.bot_management.ja3_hash eq "8b8e3d5e3e8b3d5e")
# Allow mobile app by fingerprint
(cf.bot_management.ja4 eq "your_mobile_app_fingerprint")
```
Only available for HTTPS/TLS traffic. Missing for Worker-routed traffic or HTTP requests.
## Verified Bot Categories
```txt
# Allow search engines only
(cf.verified_bot_category eq "Search Engine Crawler")
# Block AI crawlers
(cf.verified_bot_category eq "AI Crawler")
Action: Block
# Or use dashboard: Security > Settings > Bot Management > Block AI Bots
```
| Category | String Value | Example |
|----------|--------------|---------|
| AI Crawler | `AI Crawler` | GPTBot, Claude-Web |
| AI Assistant | `AI Assistant` | Perplexity-User, DuckAssistBot |
| AI Search | `AI Search` | OAI-SearchBot |
| Accessibility | `Accessibility` | Accessible Web Bot |
| Academic Research | `Academic Research` | Library of Congress |
| Advertising & Marketing | `Advertising & Marketing` | Google Adsbot |
| Aggregator | `Aggregator` | Pinterest, Indeed |
| Archiver | `Archiver` | Internet Archive, CommonCrawl |
| Feed Fetcher | `Feed Fetcher` | RSS/Podcast updaters |
| Monitoring & Analytics | `Monitoring & Analytics` | Uptime monitors |
| Page Preview | `Page Preview` | Facebook/Slack link preview |
| SEO | `Search Engine Optimization` | Google Lighthouse |
| Security | `Security` | Vulnerability scanners |
| Social Media Marketing | `Social Media Marketing` | Brandwatch |
| Webhooks | `Webhooks` | Payment processors |
| Other | `Other` | Uncategorized bots |
## Best Practices
- **ML Auto-Updates**: Enable on Enterprise for latest models
- **Start with Managed Challenge**: Test before blocking
- **Always exclude verified bots**: Use `not cf.bot_management.verified_bot`
- **Exempt corporate proxies**: For B2B traffic via `cf.bot_management.corporate_proxy`
- **Use static resource exception**: Improves performance, reduces overhead
@@ -0,0 +1,114 @@
# Bot Management Gotchas
## Common Errors
### "Bot Score = 0"
**Cause:** Bot Management didn't run (internal Cloudflare request, Worker routing to zone (Orange-to-Orange), or request handled before BM (Redirect Rules, etc.))
**Solution:** Check request flow and ensure Bot Management runs in request lifecycle
### "JavaScript Detections Not Working"
**Cause:** `js_detection.passed` always false or undefined due to: CSP headers don't allow `/cdn-cgi/challenge-platform/`, using on first page visit (needs HTML page first), ad blockers or disabled JS, JSD not enabled in dashboard, or using Block action (must use Managed Challenge)
**Solution:** Add CSP header `Content-Security-Policy: script-src 'self' /cdn-cgi/challenge-platform/;` and ensure JSD is enabled with Managed Challenge action
### "False Positives (Legitimate Users Blocked)"
**Cause:** Bot detection incorrectly flagging legitimate users
**Solution:** Check Bot Analytics for affected IPs/paths, identify detection source (ML, Heuristics, etc.), create exception rule like `(cf.bot_management.score lt 30 and http.request.uri.path eq "/problematic-path")` with Action: Skip (Bot Management), or allowlist by IP/ASN/country
### "False Negatives (Bots Not Caught)"
**Cause:** Bots bypassing detection
**Solution:** Lower score threshold (30 → 50), enable JavaScript Detections, add JA3/JA4 fingerprinting rules, or use rate limiting as fallback
### "Verified Bot Blocked"
**Cause:** Search engine bot blocked by WAF Managed Rules (not just Bot Management)
**Solution:** Create WAF exception for specific rule ID and verify bot via reverse DNS
### "Yandex Bot Blocked During IP Update"
**Cause:** Yandex updates bot IPs; new IPs unrecognized for 48h during propagation
**Solution:**
1. Check Security Events for specific WAF rule ID blocking Yandex
2. Create WAF exception:
```txt
(http.user_agent contains "YandexBot" and ip.src in {<yandex-ip-range>})
Action: Skip (WAF Managed Ruleset)
```
3. Monitor Bot Analytics for 48h
4. Remove exception after propagation completes
Issue resolves automatically after 48h. Contact Cloudflare Support if persists.
### "JA3/JA4 Missing"
**Cause:** Non-HTTPS traffic, Worker routing traffic, Orange-to-Orange traffic via Worker, or Bot Management skipped
**Solution:** JA3/JA4 only available for HTTPS/TLS traffic; check request routing
**JA3/JA4 Not User-Unique:** Same browser/library version = same fingerprint
- Don't use for user identification
- Use for client profiling only
- Fingerprints change with browser updates
## Bot Verification Methods
Cloudflare verifies bots via:
1. **Reverse DNS (IP validation):** Traditional method—bot IP resolves to expected domain
2. **Web Bot Auth:** Modern cryptographic verification—faster propagation
When `verifiedBot=true`, bot passed at least one method.
**Inactive verified bots:** IPs removed after 24h of no traffic.
## Detection Engine Behavior
| Engine | Score | Timing | Plan | Notes |
|--------|-------|--------|------|-------|
| Heuristics | Always 1 | Immediate | All | Known fingerprints—overrides ML |
| ML | 1-99 | Immediate | All | Majority of detections |
| Anomaly Detection | Influences | After baseline | Enterprise | Optional, baseline analysis |
| JavaScript Detections | Pass/fail | After JS | Pro+ | Headless browser detection |
| Cloudflare Service | N/A | N/A | Enterprise | Zero Trust internal source |
**Priority:** Heuristics > ML—if heuristic matches, score=1 regardless of ML.
## Limits
| Limit | Value | Notes |
|-------|-------|-------|
| Bot Score = 0 | Means not computed | Not score = 100 |
| First request JSD data | May not be available | JSD data appears on subsequent requests |
| Score accuracy | Not 100% guaranteed | False positives/negatives possible |
| JSD on first HTML page visit | Not supported | Requires subsequent page load |
| JSD requirements | JavaScript-enabled browser | Won't work with JS disabled or ad blockers |
| JSD ETag stripping | Strips ETags from HTML responses | May affect caching behavior |
| JSD CSP compatibility | Requires specific CSP | Not compatible with some CSP configurations |
| JSD meta CSP tags | Not supported | Must use HTTP headers |
| JSD WebSocket support | Not supported | WebSocket endpoints won't work with JSD |
| JSD mobile app support | Native apps won't pass | Only works in browsers |
| JA3/JA4 traffic type | HTTPS/TLS only | Not available for non-HTTPS traffic |
| JA3/JA4 Worker routing | Missing for Worker-routed traffic | Check request routing |
| JA3/JA4 uniqueness | Not unique per user | Shared by clients with same browser/library |
| JA3/JA4 stability | Can change with updates | Browser/library updates affect fingerprints |
| WAF custom rules (Free) | 5 | Varies by plan |
| WAF custom rules (Pro) | 20 | Varies by plan |
| WAF custom rules (Business) | 100 | Varies by plan |
| WAF custom rules (Enterprise) | 1,000+ | Varies by plan |
| Workers CPU time | Varies by plan | Applies to bot logic |
| Bot Analytics sampling | 1-10% adaptive | High-volume zones sampled more aggressively |
| Bot Analytics history | 30 days max | Historical data retention limit |
| CSP requirements for JSD | Must allow `/cdn-cgi/challenge-platform/` | Required for JSD to function |
### Plan Restrictions
| Feature | Free | Pro/Business | Enterprise |
|---------|------|--------------|------------|
| Granular scores (1-99) | No | No | Yes |
| JA3/JA4 | No | No | Yes |
| Anomaly Detection | No | No | Yes |
| Corporate Proxy detection | No | No | Yes |
| Verified bot categories | Limited | Limited | Full |
| Custom WAF rules | 5 | 20/100 | 1,000+ |
@@ -0,0 +1,182 @@
# Bot Management Patterns
## E-commerce Protection
```txt
# High security for checkout
(cf.bot_management.score lt 50 and http.request.uri.path in {"/checkout" "/cart/add"} and not cf.bot_management.verified_bot and not cf.bot_management.corporate_proxy)
Action: Managed Challenge
```
## API Protection
```txt
# Protect API with JS detection + score
(http.request.uri.path matches "^/api/" and (cf.bot_management.score lt 30 or not cf.bot_management.js_detection.passed) and not cf.bot_management.verified_bot)
Action: Block
```
## SEO-Friendly Bot Handling
```txt
# Allow search engine crawlers
(cf.bot_management.score lt 30 and not cf.verified_bot_category in {"Search Engine Crawler"})
Action: Managed Challenge
```
## Block AI Scrapers
```txt
# Block training crawlers only (allow AI assistants/search)
(cf.verified_bot_category eq "AI Crawler")
Action: Block
# Block all AI-related bots (training + assistants + search)
(cf.verified_bot_category in {"AI Crawler" "AI Assistant" "AI Search"})
Action: Block
# Allow AI Search, block AI Crawler and AI Assistant
(cf.verified_bot_category in {"AI Crawler" "AI Assistant"})
Action: Block
# Or use dashboard: Security > Settings > Bot Management > Block AI Bots
```
## Rate Limiting by Bot Score
```txt
# Stricter limits for suspicious traffic
(cf.bot_management.score lt 50)
Rate: 10 requests per 10 seconds
(cf.bot_management.score ge 50)
Rate: 100 requests per 10 seconds
```
## Mobile App Allowlisting
```txt
# Identify mobile app by JA3/JA4
(cf.bot_management.ja4 in {"fingerprint1" "fingerprint2"})
Action: Skip (all remaining rules)
```
## Datacenter Detection
```typescript
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
// Low score + not corporate proxy = likely datacenter bot
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const botMgmt = cf?.botManagement;
if (botMgmt?.score && botMgmt.score < 30 &&
!botMgmt.corporateProxy && !botMgmt.verifiedBot) {
return new Response('Datacenter traffic blocked', { status: 403 });
}
return fetch(request);
}
};
```
## Conditional Delay (Tarpit)
```typescript
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
// Add delay proportional to bot suspicion
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const botMgmt = cf?.botManagement;
if (botMgmt?.score && botMgmt.score < 50 && !botMgmt.verifiedBot) {
// Delay: 0-2 seconds for scores 50-0
const delayMs = Math.max(0, (50 - botMgmt.score) * 40);
await new Promise(r => setTimeout(r, delayMs));
}
return fetch(request);
}
};
```
## Layered Defense
```txt
1. Bot Management (score-based)
2. JavaScript Detections (for JS-capable clients)
3. Rate Limiting (fallback protection)
4. WAF Managed Rules (OWASP, etc.)
```
## Progressive Enhancement
```txt
Public content: High threshold (score < 10)
Authenticated: Medium threshold (score < 30)
Sensitive: Low threshold (score < 50) + JSD
```
## Zero Trust for Bots
```txt
1. Default deny (all scores < 30)
2. Allowlist verified bots
3. Allowlist mobile apps (JA3/JA4)
4. Allowlist corporate proxies
5. Allowlist static resources
```
## Workers: Score + JS Detection
```typescript
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const botMgmt = cf?.botManagement;
const url = new URL(request.url);
if (botMgmt?.staticResource) return fetch(request); // Skip static
// API endpoints: require JS detection + good score
if (url.pathname.startsWith('/api/')) {
const jsDetectionPassed = botMgmt?.jsDetection?.passed ?? false;
const score = botMgmt?.score ?? 100;
if (!jsDetectionPassed || score < 30) {
return new Response('Unauthorized', { status: 401 });
}
}
return fetch(request);
}
};
```
## Rate Limiting by JWT Claim + Bot Score
```txt
# Enterprise: Combine bot score with JWT validation
Rate limiting > Custom rules
- Field: lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub")
- Matches: user ID claim
- Additional condition: cf.bot_management.score lt 50
```
## WAF Integration Points
- **WAF Custom Rules**: Primary enforcement mechanism
- **Rate Limiting Rules**: Bot score as dimension, stricter limits for low scores
- **Transform Rules**: Pass score to origin via custom header
- **Workers**: Programmatic bot logic, custom scoring algorithms
- **Page Rules / Configuration Rules**: Zone-level overrides, path-specific settings
## See Also
- [gotchas.md](./gotchas.md) - Common errors, false positives/negatives, limitations
@@ -0,0 +1,78 @@
# Cloudflare Browser Rendering Skill Reference
**Description**: Expert knowledge for Cloudflare Browser Rendering - control headless Chrome on Cloudflare's global network for browser automation, screenshots, PDFs, web scraping, testing, and content generation.
**When to use**: Any task involving Cloudflare Browser Rendering including: taking screenshots, generating PDFs, web scraping, browser automation, testing web applications, extracting structured data, capturing page metrics, or automating browser interactions.
## Decision Tree
### REST API vs Workers Bindings
**Use REST API when:**
- One-off, stateless tasks (screenshot, PDF, content fetch)
- No Workers infrastructure yet
- Simple integrations from external services
- Need quick prototyping without deployment
**Use Workers Bindings when:**
- Complex browser automation workflows
- Need session reuse for performance
- Multiple page interactions per request
- Custom scripting and logic required
- Building production applications
### Puppeteer vs Playwright
| Feature | Puppeteer | Playwright |
|---------|-----------|------------|
| API Style | Chrome DevTools Protocol | High-level abstractions |
| Selectors | CSS, XPath | CSS, text, role, test-id |
| Best for | Advanced control, CDP access | Quick automation, testing |
| Learning curve | Steeper | Gentler |
**Use Puppeteer:** Need CDP protocol access, Chrome-specific features, migration from existing Puppeteer code
**Use Playwright:** Modern selector APIs, cross-browser patterns, faster development
## Tier Limits Summary
| Limit | Free Tier | Paid Tier |
|-------|-----------|-----------|
| Daily browser time | 10 minutes | Unlimited* |
| Concurrent sessions | 3 | 30 |
| Requests per minute | 6 | 180 |
*Subject to fair-use policy. See [gotchas.md](gotchas.md) for details.
## Reading Order
**New to Browser Rendering:**
1. [configuration.md](configuration.md) - Setup and deployment
2. [patterns.md](patterns.md) - Common use cases with examples
3. [api.md](api.md) - API reference
4. [gotchas.md](gotchas.md) - Avoid common pitfalls
**Specific task:**
- **Setup/deployment** → [configuration.md](configuration.md)
- **API reference/endpoints** → [api.md](api.md)
- **Example code/patterns** → [patterns.md](patterns.md)
- **Debugging/troubleshooting** → [gotchas.md](gotchas.md)
**REST API users:**
- Start with [api.md](api.md) REST API section
- Check [gotchas.md](gotchas.md) for rate limits
**Workers users:**
- Start with [configuration.md](configuration.md)
- Review [patterns.md](patterns.md) for session management
- Reference [api.md](api.md) for Workers Bindings
## In This Reference
- **[configuration.md](configuration.md)** - Setup, deployment, wrangler config, compatibility
- **[api.md](api.md)** - REST API endpoints + Workers Bindings (Puppeteer/Playwright)
- **[patterns.md](patterns.md)** - Common patterns, use cases, real examples
- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, tier limits, common errors
## See Also
- [Cloudflare Docs](https://developers.cloudflare.com/browser-rendering/)
@@ -0,0 +1,108 @@
# Browser Rendering API
## REST API
**Base:** `https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering`
**Auth:** `Authorization: Bearer <token>` (Browser Rendering - Edit permission)
### Endpoints
| Endpoint | Description | Key Options |
|----------|-------------|-------------|
| `/content` | Get rendered HTML | `url`, `waitUntil` |
| `/screenshot` | Capture image | `screenshotOptions: {type, fullPage, clip}` |
| `/pdf` | Generate PDF | `pdfOptions: {format, landscape, margin}` |
| `/snapshot` | HTML + inlined resources | `url` |
| `/scrape` | Extract by selectors | `selectors: ["h1", ".price"]` |
| `/json` | AI-structured extraction | `schema: {name: "string", price: "number"}` |
| `/links` | Get all links | `url` |
| `/markdown` | Convert to markdown | `url` |
```bash
curl -X POST '.../browser-rendering/screenshot' \
-H "Authorization: Bearer $TOKEN" \
-d '{"url":"https://example.com","screenshotOptions":{"fullPage":true}}'
```
## Workers Binding
```jsonc
// wrangler.jsonc
{ "browser": { "binding": "MYBROWSER" } }
```
## Puppeteer
```typescript
import puppeteer from "@cloudflare/puppeteer";
const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
// Content
const html = await page.content();
const title = await page.title();
// Screenshot/PDF
await page.screenshot({ fullPage: true, type: 'png' });
await page.pdf({ format: 'A4', printBackground: true });
// Interaction
await page.click('#button');
await page.type('#input', 'text');
await page.evaluate(() => document.querySelector('h1')?.textContent);
// Session management
const sessions = await puppeteer.sessions(env.MYBROWSER);
const limits = await puppeteer.limits(env.MYBROWSER);
await browser.close();
```
## Playwright
```typescript
import { launch, connect } from "@cloudflare/playwright";
const browser = await launch(env.MYBROWSER, { keep_alive: 600000 });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// Modern selectors
await page.locator('.button').click();
await page.getByText('Submit').click();
await page.getByTestId('search').fill('query');
// Context for isolation
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'custom'
});
await browser.close();
```
## Session Management
```typescript
// List sessions
await puppeteer.sessions(env.MYBROWSER);
// Connect to existing
await puppeteer.connect(env.MYBROWSER, sessionId);
// Check limits
await puppeteer.limits(env.MYBROWSER);
// { remaining: ms, total: ms, concurrent: n }
```
## Key Options
| Option | Values |
|--------|--------|
| `waitUntil` | `load`, `domcontentloaded`, `networkidle0`, `networkidle2` |
| `keep_alive` | Max 600000ms (10 min) |
| `screenshot.type` | `png`, `jpeg` |
| `pdf.format` | `A4`, `Letter`, `Legal` |
@@ -0,0 +1,78 @@
# Configuration & Setup
## Installation
```bash
npm install @cloudflare/puppeteer # or @cloudflare/playwright
```
**Use Cloudflare packages** - standard `puppeteer`/`playwright` won't work in Workers.
## wrangler.json
```json
{
"name": "browser-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"browser": {
"binding": "MYBROWSER"
}
}
```
**Required:** `nodejs_compat` flag and `browser.binding`.
## TypeScript
```typescript
interface Env {
MYBROWSER: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// ...
}
} satisfies ExportedHandler<Env>;
```
## Development
```bash
wrangler dev --remote # --remote required for browser binding
```
**Local mode does NOT support Browser Rendering** - must use `--remote`.
## REST API
No wrangler config needed. Get API token with "Browser Rendering - Edit" permission.
```bash
curl -X POST \
'https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering/screenshot' \
-H 'Authorization: Bearer TOKEN' \
-d '{"url": "https://example.com"}' --output screenshot.png
```
## Requirements
| Requirement | Value |
|-------------|-------|
| Node.js compatibility | `nodejs_compat` flag |
| Compatibility date | 2023-03-01+ |
| Module format | ES modules only |
| Browser | Chromium 119+ (no Firefox/Safari) |
**Not supported:** WebGL, WebRTC, extensions, `file://` protocol, Service Worker syntax.
## Troubleshooting
| Error | Solution |
|-------|----------|
| `MYBROWSER is undefined` | Use `wrangler dev --remote` |
| `nodejs_compat not enabled` | Add to `compatibility_flags` |
| `Module not found` | `npm install @cloudflare/puppeteer` |
| `Browser Rendering not available` | Enable in dashboard |
@@ -0,0 +1,88 @@
# Browser Rendering Gotchas
## Tier Limits
| Limit | Free | Paid |
|-------|------|------|
| Daily browser time | 10 min | Unlimited* |
| Concurrent sessions | 3 | 30 |
| Requests/minute | 6 | 180 |
| Session keep-alive | 10 min max | 10 min max |
*Subject to fair-use policy.
**Check quota:**
```typescript
const limits = await puppeteer.limits(env.MYBROWSER);
// { remaining: 540000, total: 600000, concurrent: 2 }
```
## Always Close Browsers
```typescript
const browser = await puppeteer.launch(env.MYBROWSER);
try {
const page = await browser.newPage();
await page.goto("https://example.com");
return new Response(await page.content());
} finally {
await browser.close(); // ALWAYS in finally
}
```
**Workers vs REST:** REST auto-closes after timeout. Workers must call `close()` or session stays open until `keep_alive` expires.
## Optimize Concurrency
```typescript
// ❌ 3 sessions (hits free tier limit)
const browser1 = await puppeteer.launch(env.MYBROWSER);
const browser2 = await puppeteer.launch(env.MYBROWSER);
// ✅ 1 session, multiple pages
const browser = await puppeteer.launch(env.MYBROWSER);
const page1 = await browser.newPage();
const page2 = await browser.newPage();
```
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| Session limit exceeded | Too many concurrent | Close unused browsers, use pages not browsers |
| Page navigation timeout | Slow page or `networkidle` on busy page | Increase timeout, use `waitUntil: "load"` |
| Session not found | Expired session | Catch error, launch new session |
| Evaluation failed | DOM element missing | Use `?.` optional chaining |
| Protocol error: Target closed | Page closed during operation | Await all ops before closing |
## page.evaluate() Gotchas
```typescript
// ❌ Outer scope not available
const selector = "h1";
await page.evaluate(() => document.querySelector(selector));
// ✅ Pass as argument
await page.evaluate((sel) => document.querySelector(sel)?.textContent, selector);
```
## Performance
**waitUntil options (fastest to slowest):**
1. `domcontentloaded` - DOM ready
2. `load` - load event (default)
3. `networkidle0` - no network for 500ms
**Block unnecessary resources:**
```typescript
await page.setRequestInterception(true);
page.on("request", (req) => {
if (["image", "stylesheet", "font"].includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
```
**Session reuse:** Cold start ~1-2s, warm connect ~100-200ms. Store sessionId in KV for reuse.
@@ -0,0 +1,91 @@
# Browser Rendering Patterns
## Basic Worker
```typescript
import puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
const browser = await puppeteer.launch(env.MYBROWSER);
try {
const page = await browser.newPage();
await page.goto("https://example.com");
return new Response(await page.content());
} finally {
await browser.close(); // ALWAYS in finally
}
}
};
```
## Session Reuse
Keep sessions alive for performance:
```typescript
let sessionId = await env.SESSION_KV.get("browser-session");
if (sessionId) {
browser = await puppeteer.connect(env.MYBROWSER, sessionId);
} else {
browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });
await env.SESSION_KV.put("browser-session", browser.sessionId(), { expirationTtl: 600 });
}
// Don't close browser to keep session alive
```
## Common Operations
| Task | Code |
|------|------|
| Screenshot | `await page.screenshot({ type: "png", fullPage: true })` |
| PDF | `await page.pdf({ format: "A4", printBackground: true })` |
| Extract data | `await page.evaluate(() => document.querySelector('h1').textContent)` |
| Fill form | `await page.type('#input', 'value'); await page.click('button')` |
| Wait nav | `await Promise.all([page.waitForNavigation(), page.click('a')])` |
## Parallel Scraping
```typescript
const pages = await Promise.all(urls.map(() => browser.newPage()));
await Promise.all(pages.map((p, i) => p.goto(urls[i])));
const titles = await Promise.all(pages.map(p => p.title()));
```
## Playwright Selectors
```typescript
import { launch } from "@cloudflare/playwright";
const browser = await launch(env.MYBROWSER);
await page.getByRole("button", { name: "Sign in" }).click();
await page.getByLabel("Email").fill("user@example.com");
await page.getByTestId("submit-button").click();
```
## Incognito Contexts
Isolated sessions without multiple browsers:
```typescript
const ctx1 = await browser.createIncognitoBrowserContext();
const ctx2 = await browser.createIncognitoBrowserContext();
// Each has isolated cookies/storage
```
## Quota Check
```typescript
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.remaining < 60000) return new Response("Quota low", { status: 429 });
```
## Error Handling
```typescript
try {
await page.goto(url, { timeout: 30000, waitUntil: "networkidle0" });
} catch (e) {
if (e.message.includes("timeout")) return new Response("Timeout", { status: 504 });
if (e.message.includes("Session limit")) return new Response("Too many sessions", { status: 429 });
} finally {
if (browser) await browser.close();
}
```
@@ -0,0 +1,111 @@
# C3 (create-cloudflare)
Official CLI for scaffolding Cloudflare Workers and Pages projects with templates, TypeScript, and instant deployment.
## Quick Start
```bash
# Interactive (recommended for first-time)
npm create cloudflare@latest my-app
# Worker (API/WebSocket/Cron)
npm create cloudflare@latest my-api -- --type=hello-world --ts
# Pages (static/SSG/full-stack)
npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages
```
## Platform Decision Tree
```
What are you building?
├─ API / WebSocket / Cron / Email handler
│ └─ Workers (default) - no --platform flag needed
│ npm create cloudflare@latest my-api -- --type=hello-world
├─ Static site / SSG / Documentation
│ └─ Pages - requires --platform=pages
│ npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages
├─ Full-stack app (Next.js/Remix/SvelteKit)
│ ├─ Need Durable Objects, Queues, or Workers-only features?
│ │ └─ Workers (default)
│ └─ Otherwise use Pages for git integration and branch previews
│ └─ Add --platform=pages
└─ Convert existing project
└─ npm create cloudflare@latest . -- --type=pre-existing --existing-script=./src/worker.ts
```
**Critical:** Pages projects require `--platform=pages` flag. Without it, C3 defaults to Workers.
## Interactive Flow
When run without flags, C3 prompts in this order:
1. **Project name** - Directory to create (defaults to current dir with `.`)
2. **Application type** - `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template`
3. **Platform** - `workers` (default) or `pages` (for web apps only)
4. **Framework** - If web-app: `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, etc.
5. **TypeScript** - `yes` (recommended) or `no`
6. **Git** - Initialize repository? `yes` or `no`
7. **Deploy** - Deploy now? `yes` or `no` (requires `wrangler login`)
## Installation Methods
```bash
# NPM
npm create cloudflare@latest
# Yarn
yarn create cloudflare
# PNPM
pnpm create cloudflare@latest
```
## In This Reference
| File | Purpose | Use When |
|------|---------|----------|
| **api.md** | Complete CLI flag reference | Scripting, CI/CD, advanced usage |
| **configuration.md** | Generated files, bindings, types | Understanding output, customization |
| **patterns.md** | Workflows, CI/CD, monorepos | Real-world integration |
| **gotchas.md** | Troubleshooting failures | Deployment blocked, errors |
## Reading Order
| Task | Read |
|------|------|
| Create first project | README only |
| Set up CI/CD | README → api → patterns |
| Debug failed deploy | gotchas |
| Understand generated files | configuration |
| Full CLI reference | api |
| Create custom template | patterns → configuration |
| Convert existing project | README → patterns |
## Post-Creation
```bash
cd my-app
# Local dev with hot reload
npm run dev
# Generate TypeScript types for bindings
npm run cf-typegen
# Deploy to Cloudflare
npm run deploy
```
## See Also
- **workers/README.md** - Workers runtime, bindings, APIs
- **workers-ai/README.md** - AI/ML models
- **pages/README.md** - Pages-specific features
- **wrangler/README.md** - Wrangler CLI beyond initial setup
- **d1/README.md** - SQLite database
- **r2/README.md** - Object storage
@@ -0,0 +1,71 @@
# C3 CLI Reference
## Invocation
```bash
npm create cloudflare@latest [name] [-- flags] # NPM requires --
yarn create cloudflare [name] [flags]
pnpm create cloudflare@latest [name] [-- flags]
```
## Core Flags
| Flag | Values | Description |
|------|--------|-------------|
| `--type` | `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` | Application type |
| `--platform` | `workers` (default), `pages` | Target platform |
| `--framework` | `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, `qwik`, `vue`, `angular`, `hono` | Web framework (requires `--type=web-app`) |
| `--lang` | `ts`, `js`, `python` | Language (for `--type=hello-world`) |
| `--ts` / `--no-ts` | - | TypeScript for web apps |
## Deployment Flags
| Flag | Description |
|------|-------------|
| `--deploy` / `--no-deploy` | Deploy immediately (prompts interactive, skips in CI) |
| `--git` / `--no-git` | Initialize git (default: yes) |
| `--open` | Open browser after deploy |
## Advanced Flags
| Flag | Description |
|------|-------------|
| `--template=user/repo` | GitHub template or local path |
| `--existing-script=./src/worker.ts` | Existing script (requires `--type=pre-existing`) |
| `--category=ai\|database\|realtime` | Demo filter (requires `--type=demo`) |
| `--experimental` | Enable experimental features |
| `--wrangler-defaults` | Skip wrangler prompts |
## Environment Variables
```bash
CLOUDFLARE_API_TOKEN=xxx # For deployment
CLOUDFLARE_ACCOUNT_ID=xxx # Account ID
CF_TELEMETRY_DISABLED=1 # Disable telemetry
```
## Exit Codes
`0` success, `1` user abort, `2` error
## Examples
```bash
# TypeScript Worker
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-deploy
# Next.js on Pages
npm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts
# Astro blog
npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --ts --deploy
# CI: non-interactive
npm create cloudflare@latest my-app -- --type=web-app --framework=next --ts --no-git --no-deploy
# GitHub template
npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi
# Convert existing project
npm create cloudflare@latest . -- --type=pre-existing --existing-script=./build/worker.js
```
@@ -0,0 +1,81 @@
# C3 Generated Configuration
## Output Structure
```
my-app/
├── src/index.ts # Worker entry point
├── wrangler.jsonc # Cloudflare config
├── package.json # Scripts
├── tsconfig.json
└── .gitignore
```
## wrangler.jsonc
```jsonc
{
"$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json",
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2026-01-27"
}
```
## Binding Placeholders
C3 generates **placeholder IDs** that must be replaced before deploy:
```jsonc
{
"kv_namespaces": [{ "binding": "MY_KV", "id": "placeholder_kv_id" }],
"d1_databases": [{ "binding": "DB", "database_id": "00000000-..." }]
}
```
**Replace with real IDs:**
```bash
npx wrangler kv namespace create MY_KV # Returns real ID
npx wrangler d1 create my-database # Returns real database_id
```
**Deployment error if not replaced:**
```
Error: Invalid KV namespace ID "placeholder_kv_id"
```
## Scripts
```json
{
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"cf-typegen": "wrangler types"
}
}
```
## Type Generation
Run after adding bindings:
```bash
npm run cf-typegen
```
Generates `.wrangler/types/runtime.d.ts`:
```typescript
interface Env {
MY_KV: KVNamespace;
DB: D1Database;
}
```
## Post-Creation Checklist
1. Review `wrangler.jsonc` - check name, compatibility_date
2. Replace placeholder binding IDs with real resource IDs
3. Run `npm run cf-typegen`
4. Test: `npm run dev`
5. Deploy: `npm run deploy`
6. Add secrets: `npx wrangler secret put SECRET_NAME`
Loaded 100 of 476 files, more files were not shown because too many files have changed in this diff. Show more