Specification
Self-describing structured data for AI agents and humans.
Field names encode units and semantics. Agents read latency_ms and know milliseconds, api_key_secret and know to redact — no external schema needed.
Overview
Agent-First Data has three parts:
- Naming Convention (required) — encode units and semantics in field names
- Output Processing (required) — suffix-driven formatting and automatic secret protection
- Protocol Template (optional) — structured format with
code(required) andtrace(recommended)
Parts 1 and 2 are the core. Part 3 is optional — a recommended structure that works well with Parts 1 and 2, but you can use AFDATA naming with any JSON structure (REST APIs, GraphQL, databases, etc.).
Jump to:
Quick Reference: All Suffixes
| Category | Suffixes | YAML/Plain example |
|---|---|---|
| Duration | _ns, _us, _ms, _s, _minutes, _hours, _days | latency_ms: 1280 → latency: 1.28s |
| Timestamps | _epoch_ns, _epoch_ms, _epoch_s, _rfc3339 | created_at_epoch_ms: 1707868800000 → created_at: 2024-02-14T... |
| Size | _bytes (output), _size (config input) | file_size_bytes: 5242880 → file_size: 5.0MB |
| Currency | _msats, _sats, _btc, _usd_cents, _eur_cents, _jpy, _{code}_cents | price_usd_cents: 999 → price: $9.99 |
| Other | _percent, _secret | cpu_percent: 85 → cpu: 85% |
In YAML and Plain: suffixes are stripped from keys (value already encodes the unit) and values are formatted for readability. JSON preserves original keys and raw values.
Secret protection: All three formats automatically redact _secret fields.
Boundary: AFDATA names communicate local field semantics. They do not replace schemas for required fields, enum values, numeric ranges, object shapes, or cross-field validation. Use JSON Schema, OpenAPI, database constraints, or typed APIs for those guarantees.
Part 1: Naming Convention
Applies to all structured data: JSON, YAML, TOML, CLI arguments, environment variables, config files, database columns, HTTP payload fields, log fields.
Design rules
- Name conveys meaning. A reader should understand the field’s purpose from the name alone, without seeing surrounding context or documentation.
datacould be anything —request_body,search_results,cached_responsesay exactly what it contains. - Unit in suffix. If a numeric value has a unit, encode the unit in the field name suffix.
- Secrets marked. If a value is sensitive, end the field name with
_secret. - Obvious needs no suffix. If the meaning is obvious from the name alone, no suffix is needed.
- Self-contained. Never rely on external metadata, companion fields, or documentation to convey what a field contains.
Suffixes
Duration
| Suffix | Unit | Example |
|---|---|---|
_ns | nanoseconds | gc_pause_ns: 450000 |
_us | microseconds | query_us: 830 |
_ms | milliseconds | latency_ms: 142 |
_s | seconds | dns_ttl_s: 3600 |
_minutes | minutes | session_timeout_minutes: 30 |
_hours | hours | token_validity_hours: 24 |
_days | days | cert_validity_days: 365 |
Timestamps
| Suffix | Format | Example |
|---|---|---|
_epoch_ns | nanoseconds since Unix epoch | created_epoch_ns: 1707868800000000000 |
_epoch_ms | milliseconds since Unix epoch | created_at_epoch_ms: 1707868800000 |
_epoch_s | seconds since Unix epoch | cached_epoch_s: 1707868800 |
_rfc3339 | RFC 3339 string | expires_rfc3339: "2026-02-14T10:30:00Z" |
Precision note:
_epoch_nsvalues near the current era (~1.7×10¹⁸) exceed JavaScript’s safe integer range (2⁵³ ≈ 9×10¹⁵). JSON parsed by JavaScript will silently lose nanosecond precision. UseBigIntor a custom JSON parser when nanosecond accuracy matters.
Size
| Suffix | Value type | Usage | Example |
|---|---|---|---|
_bytes | numeric | Output, APIs | payload_bytes: 456789 |
_size | string with unit | Config input | buffer_size: "10M" |
Simple rule:
- Output/APIs → use
_bytes(numeric, agents compute on this) - Config files → use
_size(string like “10M”, humans write this)
Programs parse _size at load time using parse_size() and convert to bytes for internal use.
Parsing rules for _size (binary units):
| Unit | Multiplier | Example |
|---|---|---|
B or bare number | 1 | "512" → 512 |
K | 1024 | "10K" → 10240 |
M | 1024² | "10M" → 10485760 |
G | 1024³ | "2G" → 2147483648 |
T | 1024⁴ | "1T" → 1099511627776 |
Case-insensitive. Supports decimals ("1.5M"). Returns null for invalid, negative, or overflow/unrepresentable input.
Example config file:
{
"shared_buffers_size": "128M",
"max_wal_size": "1G",
"archive_retention_size": "2T"
}
In YAML and Plain output, _bytes values auto-scale to human-readable format (5.0MB, 2.0GB).
Percentage
| Suffix | Unit | Example |
|---|---|---|
_percent | percentage | cpu_percent: 85 |
Currency
Bitcoin:
| Suffix | Unit | Example |
|---|---|---|
_msats | millisatoshis | balance_msats: 97900 |
_sats | satoshis | withdrawn_sats: 1234 |
_btc | bitcoin | reserve_btc: 0.5 |
Fiat — _{iso4217}_cents for currencies with 1/100 subdivision, _{iso4217} for currencies without (JPY). Always integers:
| Suffix | Unit | Example |
|---|---|---|
_usd_cents | US dollar cents | price_usd_cents: 999 |
_eur_cents | euro cents | price_eur_cents: 850 |
_thb_cents | Thai baht 1/100 | fare_thb_cents: 15050 |
_jpy | Japanese yen (no minor unit) | price_jpy: 1500 |
Stablecoins follow the same _{code}_cents pattern: deposit_usdt_cents: 1000, payout_usdc_cents: 500.
Sensitive
| Suffix | Handling | Example |
|---|---|---|
_secret | redact scalar values to ***; for object/array values, recursively redact nested secrets | api_key_secret: "sk-or-v1-abc..." |
All CLI output formats (JSON, YAML, Plain) automatically redact _secret fields. Scalar _secret values become ***; object/array _secret values are traversed and nested _secret fields are redacted. Matching recognizes _secret and _SECRET only. Config files always store the real value. For cases that require partial/no redaction on specific payload sections, choose an explicit output policy at serialization time. RedactionStrict is available when the entire _secret subtree must be replaced with ***.
No suffix needed
Fields whose meaning is obvious from the name alone:
- URLs:
callback_url,homepage_url - Paths:
redb_path,config_path - Counts:
proof_count,relay_count - Booleans:
search_enabled,forward_pulse - Identifiers:
method,domain,model,backend
CLI arguments
Same suffixes, kebab-case. An agent reading --help output understands units and sensitivity without documentation:
--timeout-ms 5000 # milliseconds
--cache-ttl-s 3600 # seconds
--max-size-bytes 1048576 # bytes
--api-key-secret sk-xxx # redact from logs and process listings
--buffer-size 10M # human-readable config input (parse_size)
--port 8080 # no suffix needed — meaning obvious
--verbose # boolean flag — no suffix needed
Long flags only. Do not define single-letter short flags (-s, -d, -l). Short flags are ambiguous — -s could be --synapse, --synopsis, or --source. Agents parsing --help output cannot reliably interpret single-letter aliases. Always use the full --kebab-case form. The only exception is -o for --output and built-in flags like -h/-V from the argument parser.
Kebab → snake mapping. CLI flags map 1:1 to JSON field names by replacing hyphens with underscores. When a CLI tool emits a startup log event (Part 3), the args field uses the snake_case form:
myapp --cache-ttl-s 3600 --api-key-secret sk-xxx --max-size-bytes 1048576
{"code": "log", "event": "startup", "args": {"cache_ttl_s": 3600, "api_key_secret": "***", "max_size_bytes": 1048576}}
---
code: "log"
event: "startup"
args:
api_key: "***"
cache_ttl: "3600s"
max_size: "1.0MB"
The flag name, the JSON field name, and the formatted output all tell the same story. No mapping table, no --help prose explaining “timeout is in milliseconds” — the suffix is the documentation.
Secret flags (--api-key-secret, --database-url-secret) are automatically redacted in startup messages, logs, and YAML/Plain output. Tools should also consider redacting them from /proc process listings where possible.
Complete help. --help SHOULD expand all subcommands and their flags in a single output. An agent reads myapp --help once and gets the complete interface — no crawling subcommands one by one. myapp sub --help SHOULD expand only that subcommand and its descendants.
Environment variables
Same suffixes, UPPER_SNAKE_CASE:
DATABASE_URL_SECRET=postgres://user:pass@host/db
CACHE_TTL_S=3600
TOKEN_VALIDITY_HOURS=24
RUST_LOG=info
Config files
Config files follow the same naming suffixes. Agents reading a config file can determine units, formats, and sensitivity without a separate schema.
YAML
openrouter:
api_key_secret: "sk-or-v1-actual-key"
model: "google/gemini-3-flash-preview"
storage:
backend: redb
postgres_url_secret: "postgres://user:pass@host/db"
redb_path: "data.redb"
cache:
dns_ttl_s: 3600
cmn_ttl_s: 300
pricing:
input_msats: 2
output_msats: 12
TOML
[cache]
dns_ttl_s = 3600
cmn_ttl_s = 300
[openrouter]
api_key_secret = "sk-or-v1-actual-key"
model = "google/gemini-3-flash-preview"
Database schemas
Same suffixes in column names. Agents reading a table schema can determine units, formats, and sensitivity without external documentation.
When the database type already carries semantics, no suffix is needed. TIMESTAMPTZ says “timestamp with timezone” — adding _epoch_ms is redundant. Suffixes are for generic types (BIGINT, INTEGER, TEXT) where the type alone is ambiguous.
CREATE TABLE events (
id TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL, -- type says timestamp, no suffix needed
duration_ms INTEGER, -- INTEGER is ambiguous, suffix needed
payload_bytes INTEGER,
api_key_secret TEXT,
retry_count INTEGER, -- no suffix needed, meaning is obvious
domain TEXT NOT NULL
);
| Column | Type | Suffix needed? | Why |
|---|---|---|---|
created_at | TIMESTAMPTZ | no | type encodes semantics |
duration_ms | INTEGER | yes | 142 what? ms vs s vs μs |
payload_bytes | INTEGER | yes | bytes vs KB vs count |
api_key_secret | TEXT | yes | enables auto-redaction |
retry_count | INTEGER | no | meaning obvious from name |
expires_at | TIMESTAMPTZ | no | type encodes semantics |
cached_epoch_ms | BIGINT | yes | bare integer needs unit |
ORM / struct mapping: Keep the suffix in the struct field name. The suffix is part of the semantic name, not a display concern:
struct Event {
created_at: DateTime<Utc>, // native type — no suffix
duration_ms: i64, // integer — suffix preserves semantics
// duration: i64, // bad — 64-bit what? seconds? ms?
}
Queries: Column aliases in views or query results should also follow AFDATA naming:
SELECT
duration_ms,
payload_bytes,
(cost_input_msats + cost_output_msats) AS total_cost_msats
FROM requests;
Part 2: Output Processing
Transform JSON values for CLI/log output with suffix-driven formatting and automatic secret protection. This applies to any JSON data, regardless of structure.
Two Output Paths
Path 1: Raw JSON Serialization
Return JSON values directly (for example via framework serializer or serde_json::to_string).
No output processing. Values are serialized as-is:
{"user_id": 123, "api_key_secret": "sk-1234567890abcdef", "balance_msats": 50000}
Path 2: CLI / Logs
Format JSON values for terminal/log display.
Automatic processing: Suffix formatting + secret redaction.
Input:
{"user_id": 123, "api_key_secret": "sk-1234567890abcdef", "balance_msats": 50000}
JSON: {"api_key_secret":"***","balance_msats":50000,"user_id":123}
YAML:
---
api_key: "***"
balance: "50000msats"
user_id: 123
Plain: api_key=*** balance=50000msats user_id=123
Output Formats
CLI tools should support multiple output formats:
--output json|yaml|plain
--log startup,request,progress,retry,redirect
--verbose
Default is tool-defined. Interactive CLIs default to yaml, scripting/logging contexts to json.
JSON is the canonical format. YAML and plain are derived from it.
All CLI output formats automatically redact _secret fields. Matching recognizes _secret and _SECRET only. Scalar _secret values are replaced with ***; object/array values are traversed and nested _secret fields are redacted.
Format characteristics:
- JSON — single-line, original keys, raw values, no sorting (machine-readable), secrets redacted
- YAML — multi-line, human-readable, keys stripped, values formatted, secrets redacted
- Plain — single-line logfmt, human-readable, keys stripped, values formatted, secrets redacted
yaml
Each JSON line becomes a YAML document, separated by ---. Strings always quoted to avoid YAML pitfalls (no → false, 3.0 → float). Suffixes stripped from keys (value already encodes the unit). Secrets automatically redacted.
---
code: "log"
event: "startup"
config:
api_key: "***"
dns_ttl: "3600s"
args:
config_path: "config.yml"
---
code: "ok"
result:
hash: "abc123"
size: "446.1KB"
trace:
duration: "1.28s"
cost: "2056msats"
plain
Single-line logfmt style. Suffixes stripped from keys. Secrets automatically redacted.
- Nested keys use dot notation:
trace.duration=1.28s - Values containing whitespace,
=,", or\are quoted;\,", newline, carriage return, and tab are escaped so each record stays one physical line - Arrays are comma-joined:
fields=email,age - Null values are empty:
RUST_LOG=
args.config_path=config.yml code=log event=startup config.api_key=*** config.dns_ttl=3600s
code=ok result.hash=abc123 result.size=446.1KB trace.cost=2056msats trace.duration=1.28s
Suffix processing (yaml and plain)
YAML and plain apply two transformations:
1. Key stripping — remove the suffix from the key name. The formatted value already encodes the unit, so the suffix is redundant for human readers.
Algorithm: match the longest known suffix from the list below. Each suffix is recognized in two forms: lowercase (_secret) and uppercase (_SECRET). No other casing is matched. Remove the matched suffix from the key. If no suffix matches, keep the key unchanged. Match order (longest first):
_epoch_ms,_epoch_s,_epoch_ns(compound timestamp suffixes)_usd_cents,_eur_cents,_{code}_cents(compound currency suffixes)_rfc3339,_minutes,_hours,_days(multi-char suffixes)_msats,_sats,_bytes,_percent,_secret(single-unit suffixes)_btc,_jpy,_ns,_us,_ms,_s(short suffixes, matched last to avoid false positives)
Collision: if two keys in the same object produce the same stripped key (e.g., download_bytes and download_size both → download), revert both to their original key AND raw value (no formatting).
| JSON key | YAML/Plain key | Why |
|---|---|---|
duration_ms | duration | value shows 1.28s |
size_bytes | size | value shows 446.1KB |
created_at_epoch_ms | created_at | value shows 2025-02-07T... |
expires_rfc3339 | expires | value passes through |
api_key_secret | api_key | value shows *** |
cpu_percent | cpu | value shows 85% |
balance_msats | balance | value shows 50000msats |
price_usd_cents | price | value shows $9.99 |
DATABASE_URL_SECRET | DATABASE_URL | uppercase _SECRET matched |
CACHE_TTL_S | CACHE_TTL | uppercase _S matched |
buffer_size | buffer_size | _size passes through, key unchanged |
config_path | config_path | no suffix, unchanged |
user_id | user_id | no suffix, unchanged |
2. Value formatting — transform the value for human readability. Same suffix matching as key stripping (lowercase or uppercase only):
_ns,_us,_ms,_s→ append unit (450000ns,830μs,42ms,3600s)_ms≥ 1000 → convert to seconds (1280→1.28s)_minutes,_hours,_days→ append unit (30 minutes,24 hours)_epoch_ms/_epoch_s/_epoch_ns→ RFC 3339 (2024-02-14T00:00:00.000Z), negative values produce pre-1970 dates_rfc3339→ pass through_bytes→ human-readable (456789→446.1KB,-5242880→-5.0MB)_size→ pass through (config input string, e.g."10M"stays"10M")_percent→ append%(85→85%,99.9→99.9%)_msats→ append unit (2056msats)_sats→ append unit (1234sats)_btc→ append unit (0.5 BTC)_usd_cents→ dollars (999→$9.99), negative falls through_eur_cents→ euros (850→€8.50), negative falls through- other
_{code}_cents→ major unit with code (15050→150.50 THB), negative falls through _jpy→ yen (1500→¥1,500), negative falls through_secret→***
Type constraints: _bytes and _epoch_* require integer values. _usd_cents, _eur_cents, _jpy, and _{code}_cents require non-negative integers. Duration, Bitcoin, and _percent suffixes accept any number. When the value type doesn’t match, formatting falls through to the raw value with the original key preserved.
Key ordering
YAML and plain output sort keys (after stripping) by UTF-16 code unit order (JCS, RFC 8785 §3.2.3). For ASCII keys — the common case — this equals simple byte-order sorting.
In plain logfmt, nested keys are flattened to dot notation before sorting. Sort by the full dot path: args.input_path < code < config.api_key < trace.duration.
JSON output is unordered per the JSON specification. YAML and plain sort for deterministic, cross-language-consistent output.
Using AFDATA Without Part 3
Parts 1 and 2 (naming + output processing) work with any JSON structure — no protocol template needed:
{"user_id": 123, "created_at_epoch_ms": 1738886400000, "balance_msats": 50000000, "api_key_secret": "sk-..."}
Plain: api_key=*** balance=50000000msats created_at=2025-02-07T00:00:00.000Z user_id=123
This works with REST APIs, GraphQL, database results, config files — anywhere you have structured data. Just use AFDATA naming and let output processing handle the rest.
Part 3: Protocol Template (Recommended, Optional)
A recommended structure for program output. This part is optional — adopt it when you want consistent structure across CLI tools, streaming output, or internal protocols.
Core Fields
Required:
code— identifies the message type ("log","ok","error", or tool-defined)
Recommended:
trace— execution context (duration, source, resource usage)
Everything else is flexible. Fields can be flat or nested. Both styles are valid. Examples below show both approaches.
JSONL Stream
Programs emit JSONL to stdout — one JSON object per line. Every line has a code field identifying its type:
Channel policy:
stdoutis the only protocol/log stream for machine-readable events- runtime protocol events MUST NOT be emitted on
stderr stderrmay be used only for unrecoverable pre-protocol startup failures where structured output cannot be produced
Recommended enforcement:
- Rust: clippy
print_stderr = "deny"plus disallowstd::eprintln/std::io::stderr - Go/Python/TypeScript: source-policy tests or lint rules that fail on stderr API usage in runtime code
code | Meaning |
|---|---|
"log" | Diagnostic event (event field identifies startup/request/progress/retry/redirect) |
"ok" | Success result |
"error" | Generic error (prefer specific codes) |
| tool-defined | Status / errors / progress |
Minimum logging envelope across language integrations:
- Required fields:
timestamp_epoch_ms,message,code - Optional common field:
target - Additional tool/span fields are free-form and additive
Three values are reserved: log, ok, error. All other values are tool-defined.
Error codes: Use specific codes instead of generic "error":
"not_found","unauthorized","validation_error","rate_limit","internal_error", etc.- Generic
"error"is supported but specific codes are preferred
Status codes: Progress, requests, custom events:
"request","progress","sync", etc.
Not all phases are required. A simple CLI tool may emit only a result line. A long-running service may never emit a result.
Startup Diagnostic Event
code: "log", event: "startup". Optional. Emitted once at the beginning if diagnostic logging is enabled.
{"code": "log", "event": "startup", "version": "0.1.0", "argv": ["tool", "--log", "startup"], "config": {"api_key_secret": "***", "dns_ttl_s": 3600}, "args": {"config_path": "config.yml"}, "env": {"RUST_LOG": null, "DATABASE_URL_SECRET": "***"}}
Startup payload fields are tool-defined. Common fields:
version— tool version stringargv— raw CLI argv arrayconfig— resolved configuration (recommended)args— parsed CLI arguments (optional)env— environment variables the program reads (nullif unset, optional)
Status
code is tool-defined. Content is tool-defined. Include trace for execution context.
{"code": "progress", "current": 3, "total": 10, "message": "indexing spores", "trace": {"duration_ms": 500}}
{"code": "request", "method": "POST", "path": "/v1/chat", "http_status": 200, "trace": {"latency_ms": 42}}
Result
code: "ok" on success, code: "error" or specific error code on failure. An agent watching a stream can treat any result code as the signal that the operation is complete.
Always include trace for execution context — duration, data sources, resource usage, query details.
Success - both styles valid:
Nested (structured):
{"code": "ok", "result": {"hash": "abc123", "size_bytes": 456789}, "trace": {"duration_ms": 1280, "tokens_input": 512}}
Flat:
{"code": "ok", "hash": "abc123", "size_bytes": 456789, "trace": {"duration_ms": 1280, "tokens_input": 512}}
Error - both styles valid:
Simple message:
{"code": "error", "error": "config file not found", "trace": {"duration_ms": 3}}
With actionable hint:
{"code": "error", "error": "connection refused", "hint": "check --host/--port or PGHOST/PGPORT environment variables", "trace": {"duration_ms": 3}}
The hint field is optional. When present, it provides an actionable suggestion for the user or agent to resolve the error. Omit hint when no specific remediation is available.
Nested error details:
{"code": "not_found", "error": {"resource": "user", "id": 123}, "trace": {"duration_ms": 8}}
Flat error details:
{"code": "not_found", "resource": "user", "id": 123, "trace": {"duration_ms": 8}}
More examples (flat style):
{"code": "validation_error", "fields": ["email", "age"], "trace": {"duration_ms": 2}}
{"code": "unauthorized", "message": "invalid token", "trace": {"duration_ms": 5}}
{"code": "rate_limit", "retry_after_s": 60, "quota_remaining": 0, "trace": {"duration_ms": 1}}
Best Practices
Always include trace field. Even simple operations should report execution context:
duration_ms— operation durationsource— data source (db, cache, api, file)- Resource usage —
tokens_input,tokens_output,cost_msats,memory_bytes - Metadata —
query,method,path,model
Good (with trace):
{"code": "ok", "count": 42, "trace": {"duration_ms": 150, "source": "db"}}
{"code": "error", "error": "not found", "trace": {"duration_ms": 5}}
Also good (structured):
{"code": "ok", "result": {"count": 42}, "trace": {"duration_ms": 150, "source": "db"}}
{"code": "validation_error", "error": {"fields": [...]}, "trace": {"duration_ms": 2}}
Avoid (missing trace):
{"code": "ok", "count": 42}
{"code": "error", "error": "not found"}
Missing trace makes debugging harder. Agents can’t analyze performance, cost, or data flow without execution context.
Agent consumption
- Read
codeon every line. {"code":"log","event":"startup",...}→ understand configuration."ok"or"error"→ operation complete.- Anything else → status/progress, tool-specific.
Usage in HTTP Services
The protocol structure can be used in REST APIs. Choose output path explicitly:
- raw JSON serialization for untouched payloads
- formatter output (
json|yaml|plain) when redaction/formatting is required
REST API Examples
Response body follows the protocol structure:
HTTP 200:
{"code": "ok", "result": {"balance_msats": 97900}, "trace": {"source": "redb", "duration_ms": 3}}
HTTP 404:
{"code": "not_found", "error": {"resource": "user", "id": 123}, "trace": {"duration_ms": 5}}
HTTP 402:
{"code": "insufficient_balance", "error": {"balance_msats": 0, "required_msats": 2056}, "trace": {"source": "redb", "duration_ms": 2}}
MCP Tool Response
Same structure, raw JSON:
{"code": "ok", "result": {"files": ["src/main.rs"]}, "trace": {"source": "glob", "matched": 1, "duration_ms": 12}}
Streaming (SSE)
JSONL stream, raw JSON per line:
{"code": "log", "event": "startup", "config": {"model": "gpt-4", "max_tokens": 1024}, "args": {}, "env": {}}
{"code": "progress", "current": 1, "total": 5, "message": "processing", "trace": {"duration_ms": 500}}
{"code": "ok", "result": {"answer": "..."}, "trace": {"tokens_input": 512, "duration_ms": 1280}}
One Protocol, Multiple Contexts
| Context | Output | Secret Protection |
|---|---|---|
| CLI / Logs | JSONL (json/yaml/plain formats) | ✅ Automatic |
| HTTP body (raw path) | JSON body (raw Value) | Use redacted_value before framework serialization |
| MCP tool (raw path) | JSON (raw Value) | Use redacted_value before SDK serialization |
| SSE stream (raw path) | JSONL (raw JSON) | Use redacted_value before emitting events |
All contexts can use the protocol structure from Part 3. Only code (required) and trace (recommended) are standardized. Other fields can be flat or nested — both styles work. CLI/logs apply output formatting and secret protection from Part 2. Raw-path serializers return JSON values unchanged unless the program explicitly calls redacted_value. For CLI/log protocol transport, use stdout only; do not split protocol events across stdout and stderr.
Complete Example: CLI Tool
A complete example showing all three parts working together. A backup tool that uploads files to cloud storage.
CLI Invocation
cloudback --api-key-secret sk-1234567890abcdef --timeout-s 30 --max-file-size-bytes 10737418240 /data/backup.tar.gz
Flag names use AFDATA suffixes in kebab-case. An agent reading --help knows --timeout-s is seconds and --api-key-secret should be redacted — no documentation needed.
Raw JSON (before output processing)
The tool converts CLI flags from kebab-case to snake_case and emits a startup diagnostic event when enabled:
{
"code": "log",
"event": "startup",
"config": {
"api_key_secret": "sk-1234567890abcdef",
"endpoint": "https://storage.example.com",
"timeout_s": 30,
"max_file_size_bytes": 10737418240
},
"args": {
"input_path": "/data/backup.tar.gz",
"compression_level": 9
}
}
Field names encode semantics:
api_key_secret→ agent knows to redacttimeout_s→ 30 secondsmax_file_size_bytes→ 10GB in bytes
Output Formats (Part 2: Output Processing)
JSON (raw, for machines):
{"code":"log","event":"startup","config":{"api_key_secret":"***","endpoint":"https://storage.example.com","timeout_s":30,"max_file_size_bytes":10737418240},"args":{"input_path":"/data/backup.tar.gz","compression_level":9}}
YAML (structured, keys stripped, for human inspection):
---
code: "log"
event: "startup"
args:
compression_level: 9
input_path: "/data/backup.tar.gz"
config:
api_key: "***"
endpoint: "https://storage.example.com"
max_file_size: "10.0GB"
timeout: "30s"
Plain (single-line logfmt, keys stripped, for compact scanning):
args.compression_level=9 args.input_path=/data/backup.tar.gz code=log event=startup config.api_key=*** config.endpoint=https://storage.example.com config.max_file_size=10.0GB config.timeout=30s
Note:
- Key stripping:
api_key_secret→api_key,timeout_s→timeout,max_file_size_bytes→max_file_size - Secret protection:
api_key_secretredacted in all three formats - Suffix formatting:
_bytes→10.0GB,_s→30sin YAML and Plain
Progress Update (Part 3: Protocol Template)
{"code": "progress", "current": 3, "total": 10, "message": "uploading chunks", "trace": {"duration_ms": 5420, "uploaded_bytes": 3221225472}}
YAML:
---
code: "progress"
current: 3
message: "uploading chunks"
total: 10
trace:
duration: "5.42s"
uploaded: "3.0GB"
Plain:
code=progress current=3 message="uploading chunks" total=10 trace.duration=5.42s trace.uploaded=3.0GB
Final Result
{"code": "ok", "result": {"url": "https://storage.example.com/backup.tar.gz", "size_bytes": 10485760, "checksum": "sha256:abc123...", "uploaded_at_epoch_ms": 1738886400000}, "trace": {"duration_ms": 15300, "chunks": 10, "retries": 2}}
YAML:
---
code: "ok"
result:
checksum: "sha256:abc123..."
size: "10.0MB"
uploaded_at: "2025-02-07T00:00:00.000Z"
url: "https://storage.example.com/backup.tar.gz"
trace:
chunks: 10
duration: "15.3s"
retries: 2
Plain:
code=ok result.checksum=sha256:abc123... result.size=10.0MB result.uploaded_at=2025-02-07T00:00:00.000Z result.url=https://storage.example.com/backup.tar.gz trace.chunks=10 trace.duration=15.3s trace.retries=2
What This Demonstrates
-
Part 1 (Naming): Every field is self-describing — from CLI flags (
--timeout-s,--api-key-secret) to JSON fields (timeout_s,uploaded_at_epoch_ms). Same suffixes, same semantics, kebab↔snake mapping -
Part 2 (Output Processing): Three formats for different needs
- JSON: single-line, original keys, raw values, for programs and logs
- YAML: multi-line, keys stripped, values formatted, for human inspection
- Plain: single-line logfmt, keys stripped, values formatted, for compact scanning
- All formats protect secrets automatically
-
Part 3 (Protocol): Consistent structure across all output —
codeidentifies message type,traceprovides execution context, other fields flexible
Key insight: The same naming convention flows from CLI flag (--timeout-s 30) to JSON field (timeout_s: 30) to formatted output (timeout: 30s). An agent reading --help, JSON output, or YAML all gets the same self-describing semantics — no documentation needed at any layer.