How the Cloudflare cf CLI Uses JSON So an AI Agent Can Search and Call an API (2026)
An agent should not memorize more than 2,900 commands. It searches with one sentence, reads the API fields that command will send, and only then runs it. The result on standard output is indented JSON. Progress and errors go to standard error. The model only fills arguments. cf is what calls the Cloudflare API.
Before you read
| Object | Public fact as of 2026-10-13 | How an agent uses it |
|---|---|---|
| cf | Open beta on 28 September. Covers the whole API. Docs say more than 2,900 commands. Install with npm | JSON is the default. No --json flag |
| cf cli search | Local search. No login. The whole task must be quoted. Up to five matches, best first | Take command and summary. Do not execute yet |
| cf schema | Describes the HTTP a generated command will send: method, path, parameters, body fields | Fill required fields first. deploy and similar commands use --help |
| Command result | 29 September docs: JSON is always indented. A non-interactive delete without --force can print Aborted. on stderr and still exit 0 | Read the stdout JSON, then the exit code |
The launch post said pretty-printed for humans and condensed for agents. The 29 September docs say JSON is indented whether or not stdout is a terminal. A public check of 1.0.0-beta.5 on 8 October also saw no condensed output. Parsers should follow the docs. The name orders.example.test is fictional.
An agent drives Cloudflare in three JSON steps, not by memorizing commands
Cloudflare published cf on 28 September. The launch post said agents were about a quarter of Wrangler use in March and 48% the week before launch. Wrangler had about 280 hand-built commands and could not cover the whole API. The new CLI is generated from OpenAPI. The 29 September docs say it has more than 2,900 commands. Install with npm install --global cf. cf auth login needs a person to approve a browser page. Unattended runs set CLOUDFLARE_API_TOKEN, which takes priority over a stored login. Do not commit the token. The sample placeholder is sk_demo_not_real.
- Search:
cf cli search "create D1 database". The quotes stay. It runs locally and does not touch the account. - Inspect:
cf schema d1 createlists the HTTP fields that will be sent. Add--dry-runbefore a real change. A dry run needs no credentials. - Run: standard output is only the JSON result. The selected zone, errors, and
Aborted.go to standard error.
This is not an agent clicking through a page. How a plugin directory places an MCP server in mcp.json: the Agent Plugins 1.0.0 directory contract. What an MCP tools/call looks like: how a coding agent calls MCP. This piece only covers the JSON cf hands to an agent.
Search results are an array of at most five objects
The 29 September docs show the first two matches in this shape. The only fields are command and summary:
[
{
"command": "cf d1 create",
"summary": "Create D1 Database"
},
{
"command": "cf d1 update",
"summary": "Update D1 Database"
}
]
Without quotes, every word after the first is treated as an unknown command. Root and group --help starts by telling the agent to search. A command’s own --help is plain text, not this array. If the command does not exist, the CLI lists close names. That list is not a search result.
Read the request that will be sent, then the one page that came back
cf schema covers generated API commands. cf deploy needs --help. The documented keys are operationId, httpMethod, path, pathParams, queryParams, hasRequestBody, and requestBodyFields. Parameters use API names such as account_id. Body fields use option names and include a description. Some body lists are incomplete. Pass the full body with --body. The object below uses those same keys for a fictional hostname. It is not a capture from a production account:
{
"operationId": "dns-records-create",
"httpMethod": "POST",
"path": "/zones/{zone_id}/dns_records",
"pathParams": [
{ "name": "zone_id", "type": "string", "required": true }
],
"queryParams": [],
"hasRequestBody": true,
"requestBodyFields": [
{ "name": "type", "type": "string", "required": true, "description": "Record type" },
{ "name": "name", "type": "string", "required": true, "description": "Record name" },
{ "name": "content", "type": "string", "required": true, "description": "Record content" }
]
}
Required fields pass a gate before the command runs. Why that gate cannot live only in the prompt: why AI needs JSON Schema. A list command prints one page as a JSON array. Paging flags differ by command. A change that returns no data prints nothing. An R2 object or a model image is written to stdout unchanged, not as an object. One fictional page of DNS records:
[
{
"id": "rec_demo_1042",
"type": "TXT",
"name": "orders.example.test",
"content": "demo-pass"
}
]
Field names follow the command. Confirm the value is an array, then take id or name from that command’s schema. Do not assume every page has a success envelope.
stdout, stderr, and the exit code are not one success signal
An agent usually collapses three different signals:
| What you see | What it means |
|---|---|
| Indented JSON on stdout | That is the result. The docs say it is indented in a terminal and outside one. Do not treat the launch line “condensed for agents” as the current parse contract. |
| Selected zone, errors, Aborted. | Those go to stderr. In a non-interactive session, a destructive command without --force can print Aborted. and still exit 0. |
| cf workers delete --force | On some commands --force is also an API parameter. It can delete a Worker that other Workers still reference. Read the schema before allowing it. |
| A repo that only has wrangler.jsonc | Do not let an agent run cf dev, cf build, or cf deploy until cloudflare.config.ts exists. Start with cf migrate --dry-run. |
A project that already has Wrangler config keeps using Wrangler. Other Cloudflare tasks go through cf. That sentence belongs in AGENTS.md or CLAUDE.md. --local covers only a few local resources, such as some KV, D1, and R2 paths. cf d1 query has no local equivalent and returns an error instead of quietly calling the live API.
Six steps before an agent changes a live resource
Do not start by pasting a token into the prompt:
- A person finishes cf auth login. Set
CLOUDFLARE_API_TOKENonly for unattended runs. The token does not belong in the command JSON. - Search with quotes. The whole task is one argument. Read
commandfrom at most five objects. - Run cf schema. Check
required. If the list is incomplete, switch to--bodyand the API reference. - Run --dry-run first. It prints the request JSON, sends nothing, and needs no credentials. Confirm the path uses a demo value such as
zone_demo_1042. - Parse stdout only. Empty stdout means the change returned no data. For a destructive command, look for
Aborted.on stderr. - Migrate a Wrangler project first. Do not deploy while
cloudflare.config.tsis missing. The build fails until everyTODO(@cloudflare)in the generated file is resolved.
After the open beta, Wrangler still gets 18 months of maintenance. The two tools can coexist. Do not let an agent cover an old config with cf deploy in the same project.
Spread three JSON values before you allow a write
Most failures are an unquoted search, a schema that does not match the result, or an exit code 0 read as a completed delete.
- format the search array and the schema, then Schema validate whether every
requestBodyFieldsitem withrequired: trueis present. - Use JSONPath extract for
$[0].command,$.httpMethod, and$.path. - JSON Diff the
--dry-runrequest against the page that came back. Fill missing fields before you rewrite the prompt. - If stderr contains
Aborted., the delete did not happen. - Do not paste zones, tokens, or account ids into a public formatter. JSONSort runs in the browser.
When those three JSON values line up, the agent is calling an API rather than guessing a command. Indentation is not an error.
Common traps
- Waiting for condensed JSON because the launch post said so. The current docs say the JSON is indented. The 8 October check of beta.5 also saw no shorter output when an agent was detected.
- Searching without quotes. The second word becomes an unknown command. A search result and a list of close command names are not the same JSON.
- Reading exit code 0 as a finished delete. Without
--forcein a non-interactive session, the command may only have aborted. Read stderr too. - Running cf deploy in a repo that only has wrangler.jsonc. It can generate a new config and leave the old one unused. Dry-run the migration first.
- Putting an API token in the prompt or in committed JSON. Unattended runs use the environment variable. Samples do not use a real token.
- Writing cf as an agent that clicks a browser by itself. Login needs a person. Later API calls are JSON from the CLI, not page actions.
FAQ
Does an agent still add --json to every command?
No. cf results are JSON on stdout by default. Wrangler is the tool where some commands needed --json and others printed tables. The 29 September docs say this JSON is indented. Do not wait for a compact format.
Does cf cli search use my account?
No. Search is local and needs no credentials. It returns at most five objects, each with command and summary. The later command is what changes a resource.
If there is no JSON, did the command fail?
Not always. A change that returns no data leaves stdout empty. Failure is a non-zero exit code, with the error on stderr. An aborted delete prints Aborted. on stderr and can still exit 0.
Do I have to switch a Wrangler project to cf today?
Not the deploy path, on the same day. With wrangler.jsonc or wrangler.toml, leave dev, build, and deploy on Wrangler. Move to cloudflare.config.ts with cf migrate, and add --dry-run first. Wrangler keeps 18 months of maintenance after the open beta.
What if requestBodyFields is incomplete?
The docs say some commands return an incomplete or empty requestBodyFields list. Pass the full body with --body and check the API reference. An empty list does not mean the API has no body.
Conclusion
cf lets an AI agent call the Cloudflare API with JSON: search returns command names, schema returns the fields that will be sent, and stdout returns one page of results. Parse indented JSON, and keep stderr and the exit code separate.
Check that cf d1 create came from the search array, zone_id came from the schema, and orders.example.test appears only in a demo result. When those three line up, let it change a live resource.
Further reading
- plugin.json in Agent Plugins 1.0.0 A different fixed place for MCP config, not this CLI
- How a coding agent calls MCP The wire shape of tools/call
- Why AI needs JSON Schema Why a required field cannot live only in the prompt
- JSONSort local toolbox Format, validate, Diff, JSONPath
- All posts More notes on JSON and AI development
Changelog: First published