Don't rewrite your CLI for agents - Microsoft for Developers
Skip to main content
Dev Blogs
AI
All .NET posts
.NET MAUI<br>ASP.NET Core<br>Blazor<br>Entity Framework
C++<br>C#<br>F#<br>TypeScript
NuGet<br>Servicing<br>.NET Blog in Chinese
Microsoft for Developers<br>Agent Framework<br>Develop from the cloud<br>Xcode<br>ISE Developer<br>TypeScript<br>PowerShell<br>Python<br>Java<br>Java Blog in Chinese<br>Go<br>Microsoft Edge Dev<br>Microsoft 365 Developer<br>Microsoft Entra Identity Developer<br>Microsoft Entra PowerShell
Visual Studio<br>Visual Studio Code<br>Aspire
All things Azure<br>Azure SDK<br>Azure VM Runtime Team<br>Microsoft Azure<br>Azure Cosmos DB<br>Azure DocumentDB<br>Azure Data Studio<br>Azure SQL<br>DevOps<br>DirectX<br>Microsoft Foundry<br>Power Platform
OData<br>Unified Data Model (IDEAs)
Windows Command Line<br>#ifdef Windows<br>Inside MSIX<br>MIDI and music<br>React Native<br>The Old New Thing<br>Windows Developer
Waldek Mastykarz
Principal Developer Advocate
There’s advice making the rounds: replace your CLI args with a single --json payload so agents can use your tool more effectively. The thinking being, that agents already think in structured formats, and nested data maps cleanly to JSON. Flat args on the other hand, force awkward conventions like repeating --service-name to delimit multi-value groups, which is inherently ambiguous. Not to mention, that the agent needs to get the types of all values right.
It’s a reasonable hypothesis, and we wanted to know if it holds up under measurement. The data we collected, showed something interesting.
What we tested
We built a synthetic CLI called podctl that creates multi-service deployments. The scenario was deliberately complex: two services with independent configuration, three levels of nesting (services[].resources.cpu.request), arrays, mixed types, and cross-references between services. Over 30 distinct values across the deployment spec. Here’s what the args invocation looks like:
podctl create \<br>--name "api-gateway-prod" \<br>--env production \<br>--region us-east-1 \<br>--replicas 3 \<br>--service-name auth \<br>--service-image ghcr.io/acme/auth:v2.4.1 \<br>--service-port 8080 \<br>--service-protocol grpc \<br>--service-cpu-request 250m \<br>--service-cpu-limit 500m \<br>--service-mem-request 128Mi \<br>--service-mem-limit 256Mi \<br>--service-health-path /healthz \<br>--service-health-interval 10s \<br>--service-health-timeout 3s \<br>--service-health-retries 3 \<br>--service-env DB_HOST=db.internal \<br>--service-env DB_PORT=5432 \<br>--service-name gateway \<br>--service-image ghcr.io/acme/gateway:v1.8.0 \<br>--service-port 443 \<br>--service-protocol https \<br>--service-cpu-request 500m \<br>--service-cpu-limit 1000m \<br>--service-mem-request 256Mi \<br>--service-mem-limit 512Mi \<br>--service-health-path /ready \<br>--service-health-interval 15s \<br>--service-health-timeout 5s \<br>--service-health-retries 5 \<br>--service-env UPSTREAM=http://auth:8080 \<br>--service-depends-on auth \<br>--scaling-min 2 \<br>--scaling-max 10 \<br>--scaling-metric cpu \<br>--scaling-target 70 \<br>--rollout-strategy canary \<br>--rollout-canary-percent 10 \<br>--rollout-canary-pause 300
Notice how --service-name appears twice to start a new service block. The agent has to figure out that flags after the second --service-name belong to the gateway, not to auth. And here’s the same deployment as a JSON payload:
podctl create --json '{<br>"name": "api-gateway-prod",<br>"environment": "production",<br>"region": "us-east-1",<br>"replicas": 3,<br>"services": [<br>"name": "auth",<br>"image": "ghcr.io/acme/auth:v2.4.1",<br>"port": 8080,<br>"protocol": "grpc",<br>"resources": {<br>"cpu": {"request": "250m", "limit": "500m"},<br>"memory": {"request": "128Mi", "limit": "256Mi"}<br>},<br>"healthCheck": {<br>"path": "/healthz",<br>"interval": "10s",<br>"timeout": "3s",<br>"retries": 3<br>},<br>"env": {"DB_HOST": "db.internal", "DB_PORT": "5432"}<br>},<br>"name": "gateway",<br>"image": "ghcr.io/acme/gateway:v1.8.0",<br>"port": 443,<br>"protocol": "https",<br>"resources": {<br>"cpu": {"request": "500m", "limit": "1000m"},<br>"memory": {"request": "256Mi", "limit": "512Mi"}<br>},<br>"healthCheck": {<br>"path": "/ready",<br>"interval": "15s",<br>"timeout": "5s",<br>"retries": 5<br>},<br>"env": {"UPSTREAM": "http://auth:8080"},<br>"dependsOn": ["auth"]<br>],<br>"scaling": {<br>"min": 2,<br>"max": 10,<br>"metric": "cpu",<br>"targetPercent": 70<br>},<br>"rollout": {<br>"strategy": "canary",<br>"canaryPercent": 10,<br>"canaryPauseSec": 300<br>}'
You can see the appeal. The JSON version makes the hierarchy explicit. Each service is a distinct object, resources nest naturally, and there’s no ambiguity about which field belongs where. On paper, this should be easier for an agent to get right.
We used two separate CLIs as the test subjects. One accepts only individual args, the other accepts only a --json payload. Both share the same validation backend and normalize to the same canonical structure. At test time, whichever variant is active gets renamed to podctl so the agent sees a consistent binary name. Since the CLI is unknown to the agent, it would have to discover the input model from scratch. For args, it means invoking the CLI’s help. For JSON, it also means invoking help but also calling an extra command that...