AI Development

How the Cloudflare cf CLI Uses JSON So an AI Agent Can Search and Call an API (2026)

About 15 min read

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 create lists the HTTP fields that will be sent. Add --dry-run before 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.

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:

  1. A person finishes cf auth login. Set CLOUDFLARE_API_TOKEN only for unattended runs. The token does not belong in the command JSON.
  2. Search with quotes. The whole task is one argument. Read command from at most five objects.
  3. Run cf schema. Check required. If the list is incomplete, switch to --body and the API reference.
  4. 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.
  5. Parse stdout only. Empty stdout means the change returned no data. For a destructive command, look for Aborted. on stderr.
  6. Migrate a Wrangler project first. Do not deploy while cloudflare.config.ts is missing. The build fails until every TODO(@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 requestBodyFields item with required: true is present.
  • Use JSONPath extract for $[0].command, $.httpMethod, and $.path.
  • JSON Diff the --dry-run request 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 --force in 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

Changelog: First published