Agent Skill


name: agent-first-data description: Apply Agent-First Data naming and output conventions when writing structured data, configs, logs, transport payloads, or CLI output in any language. disable-model-invocation: true allowed-tools: Bash, Read, Edit, Write, Glob, Grep

This skill content is tool-agnostic. It can be used by any AI coding agent workflow. The frontmatter keys at the top are metadata for skill runners and do not change the AFDATA conventions.

Three parts:

  1. Naming — encode units and semantics in field names so agents parse structured data without external schemas
  2. Output — suffix-driven formatting with key stripping, value formatting, and automatic secret redaction
  3. Protocol — optional JSONL protocol with code (required) and trace (recommended)

Part 1: Naming Convention

The field name is the schema. Always encode units and semantics in the field name.

Duration

SuffixUnitExample
_nsnanosecondsgc_pause_ns: 450000
_usmicrosecondsquery_us: 830
_msmillisecondslatency_ms: 142
_ssecondsdns_ttl_s: 3600
_minutesminutessession_timeout_minutes: 30
_hourshourstoken_validity_hours: 24
_daysdayscert_validity_days: 365

Timestamps

SuffixFormatExample
_epoch_msmilliseconds since Unix epochcreated_at_epoch_ms: 1707868800000
_epoch_sseconds since Unix epochcached_epoch_s: 1707868800
_epoch_nsnanoseconds since Unix epochcreated_epoch_ns: 1707868800000000000
_rfc3339RFC 3339 stringexpires_rfc3339: "2026-02-14T10:30:00Z"

Size

SuffixExample
_bytespayload_bytes: 456789 (always numeric)
_sizebuffer_size: "10M" (config files only, human-readable)

_size parsing rules (binary): B=1, K=1024, M=1024², G=1024³, T=1024⁴. Case-insensitive.

parse_size("10M")10485760. Returns null for invalid or negative input.

Percentage

SuffixExample
_percentcpu_percent: 85

Currency

Bitcoin:

SuffixExample
_msatsbalance_msats: 97900
_satswithdrawn_sats: 1234
_btcreserve_btc: 0.5

Fiat — _{iso4217}_cents for currencies with 1/100 subdivision, _{iso4217} for currencies without:

SuffixExample
_usd_centsprice_usd_cents: 999
_eur_centsprice_eur_cents: 850
_jpyprice_jpy: 1500
_usdt_centsdeposit_usdt_cents: 1000

Sensitive

SuffixHandlingExample
_secretredact to ***api_key_secret: "sk-or-v1-abc..."

All CLI output formats (JSON, YAML, Plain) automatically redact _secret fields. Matching recognizes _secret and _SECRET only — no mixed case.

Environment variables

Same suffixes, UPPER_SNAKE_CASE:

DATABASE_URL_SECRET=postgres://user:pass@host/db
CACHE_TTL_S=3600
TOKEN_VALIDITY_HOURS=24

No suffix needed

Fields whose meaning is obvious: callback_url, redb_path, proof_count, search_enabled, method, domain, model.

Database columns

Use suffixes on generic types (INTEGER, BIGINT, TEXT). Native types that carry semantics (TIMESTAMPTZ, INTERVAL) don’t need suffixes.

ColumnTypeSuffix?Why
created_atTIMESTAMPTZnotype says timestamp
duration_msINTEGERyesinteger is ambiguous
api_key_secretTEXTyesenables auto-redaction
retry_countINTEGERnomeaning obvious

ORM struct fields preserve the suffix: duration_ms: i64, not duration: i64.

Common mistakes

BadGoodWhy
timeout: 30timeout_s: 3030 what? seconds? ms?
timestamp: 1707868800cached_epoch_s: 1707868800what unit? what event?
size: 456789payload_bytes: 456789bytes? KB?
price: 999price_usd_cents: 999what currency? what unit?
latency: 142latency_ms: 142seconds? milliseconds?
api_key: "sk-..."api_key_secret: "sk-..."won’t be auto-redacted
cpu: 85cpu_percent: 8585 what?
buffer: "10M"buffer_size: "10M"only _size gets parsed

Part 2: Output Processing

Three output formats. YAML and Plain apply key stripping + value formatting.

Formats

Key stripping (YAML and Plain)

Remove recognized suffix from key. Longest match first, exact lowercase or uppercase only:

  1. _epoch_ms, _epoch_s, _epoch_ns
  2. _usd_cents, _eur_cents, _{code}_cents
  3. _rfc3339, _minutes, _hours, _days
  4. _msats, _sats, _bytes, _percent, _secret
  5. _btc, _jpy, _ns, _us, _ms, _s

_size is NOT stripped (pass through). If two keys collide after stripping, both revert to original key AND raw value (no formatting).

Value formatting (YAML and Plain)

Type constraints: _bytes/_epoch_* require integer. _usd_cents/_eur_cents/_jpy/_{code}_cents require non-negative integer. Duration/Bitcoin/_percent accept any number. Wrong type → raw value + original key.

Plain logfmt details

Key ordering

YAML and Plain sort keys (after stripping) by UTF-16 code unit order (JCS, RFC 8785). For ASCII keys this equals byte-order sorting.


Part 3: Protocol Template (Optional)

Every output line carries a code field:

codeWhen
"log"Diagnostic event (event field identifies startup/request/progress/retry/redirect)
tool-definedStatus/progress ("request", "progress", "sync", etc.)
"ok"Success result
"error"Error result

Channel policy:

Recommended enforcement:

Templates

{"code": "log", "event": "startup", "version": "0.1.0", "argv": ["tool", "--log", "startup"], "config": {...}, "args": {...}, "env": {...}}
{"code": "ok", "result": {...}, "trace": {"duration_ms": 12, "source": "redb"}}
{"code": "error", "error": "message", "trace": {"duration_ms": 3}}
{"code": "not_found", "resource": "user", "id": 123, "trace": {"duration_ms": 8}}

Always include trace for execution context: duration, token counts, cost, data source. Startup payload fields are tool-defined; config is recommended, while version/argv/args/env are optional.

Same structure, any transport

TransportFormat
CLI stdoutJSONL
REST APIJSON body
MCP toolJSON
SSE streamJSONL

All use code / result / error / trace. Do not split protocol events across stdout and stderr.


Using the Library

15 public APIs and 2 types (same across all languages):

Function / TypeWhat it does
build_json_okBuild {code: "ok", result, trace?}
build_json_errorBuild {code: "error", error, trace?}
build_jsonBuild {code: "<custom>", ...fields, trace?}
redacted_valueJSON-safe copy with default _secret redaction
redacted_value_withJSON-safe copy with explicit redaction policy
output_jsonSingle-line JSON, secrets redacted, original keys
output_json_withSingle-line JSON with explicit redaction policy
output_yamlMulti-line YAML, keys stripped, values formatted
output_plainSingle-line logfmt, keys stripped, values formatted
internal_redact_secretsRedact _secret fields in-place
parse_sizeParse "10M" → bytes
OutputFormat"json" / "yaml" / "plain" enum/type
RedactionPolicyRedactionTraceOnly / RedactionNone / RedactionStrict enum/type
cli_parse_outputParse --output flag; error on unknown value
cli_parse_log_filtersNormalize --log entries: trim, lowercase, dedup, remove empty
cli_outputDispatch to output_json / output_yaml / output_plain
build_cli_error{code:"error", error_code:"invalid_request", retryable:false, trace:{duration_ms:0}}

Rust

use agent_first_data::{build_json_ok, build_json_error, build_json, output_json, output_yaml, output_plain, internal_redact_secrets, parse_size};
use agent_first_data::{OutputFormat, cli_parse_output, cli_parse_log_filters, cli_output, build_cli_error};

Python

from agent_first_data import build_json_ok, build_json_error, build_json, output_json, output_yaml, output_plain, internal_redact_secrets, parse_size
from agent_first_data import OutputFormat, cli_parse_output, cli_parse_log_filters, cli_output, build_cli_error

TypeScript

import { buildJsonOk, buildJsonError, buildJson, outputJson, outputYaml, outputPlain, internalRedactSecrets, parseSize } from "agent-first-data";
import { type OutputFormat, cliParseOutput, cliParseLogFilters, cliOutput, buildCliError } from "agent-first-data";

Go

import afdata "github.com/agentfirstkit/agent-first-data/go"

afdata.OutputPlain(value)
afdata.ParseSize("10M")
afdata.CliParseOutput("json")
afdata.BuildCliError("--output: invalid value 'xml'")

CLI Helpers Pattern

When building a CLI tool on AFDATA, always use the CLI helpers to parse --output and --log flags. This ensures consistent behavior and error format across all tools:

--output json|yaml|plain    → cli_parse_output
--log startup,request,...   → cli_parse_log_filters (trim, lowercase, dedup, remove empty)
parse errors                → build_cli_error + output_json + exit 2

Key rule: use try_parse() / try_parse_from() (not parse()) in Rust/clap so that parse errors go to stdout as JSONL, not stderr as plain text.

AFDATA Logging

Structured logging that outputs via the library’s own output_json/output_plain/output_yaml. Each language integrates with its native logging ecosystem. All three formats apply the same suffix processing, key stripping, and secret redaction as the core output API.

Init (pick one format per process)

FormatRustGoPythonTypeScript
JSONafdata_tracing::init_json(filter)afdata.InitJson()init_logging_json("INFO")initJson()
Plainafdata_tracing::init_plain(filter)afdata.InitPlain()init_logging_plain("INFO")initPlain()
YAMLafdata_tracing::init_yaml(filter)afdata.InitYaml()init_logging_yaml("INFO")initYaml()

Rust requires cargo add agent-first-data --features tracing.

Spans (add fields to all log events in scope)

// Rust — tracing spans
let span = info_span!("request", request_id = %uuid);
let _guard = span.enter();
// Go — context-based
ctx := afdata.WithSpan(ctx, map[string]any{"request_id": uuid})
logger := afdata.LoggerFromContext(ctx)
# Python — contextvars
with span(request_id=uuid):
    logger.info("Processing")
// TypeScript — AsyncLocalStorage
await span({ request_id: uuid }, async () => {
  log.info("Processing");
});

Output fields

Every log line contains: timestamp_epoch_ms, message, code (defaults to log level, overridable), plus span fields and event fields.

CLI Flags

CLI tools that use AFDATA should support output and logging flags:

--output json|yaml|plain    # default is tool-defined (interactive → yaml, scripting/logging → json)
--log startup,request,progress,retry,redirect
--verbose                   # shorthand for all log categories

Review Checklist

When reviewing code that produces structured output:

  1. Every numeric field with a unit has the correct suffix (_ms, _bytes, _sats, _percent, etc.)
  2. Timestamps use _epoch_ms / _epoch_s / _rfc3339 — never bare timestamp: 1707868800
  3. Sensitive values end in _secret and are redacted in all output paths
  4. Transport payloads / CLI output use code / result / error / trace structure
  5. Config files use the same suffixes as output
  6. No unit-less ambiguous fields (timeout: 30 — 30 what?)
  7. Config size values use _size suffix (buffer_size: "10M", not buffer: "10M")
  8. Environment variables follow UPPER_SNAKE_CASE with the same suffixes
  9. Logging uses AFDATA init functions (init_json/init_plain/init_yaml) — not raw println!/fmt.Println/console.log for structured output
  10. Database columns use AFDATA suffixes on generic types (duration_ms INTEGER, not duration INTEGER); native types like TIMESTAMPTZ don’t need suffixes
  11. CLI flag parsing uses cli_parse_output/cli_parse_log_filters/build_cli_error — not custom reimplementations; uses try_parse() not parse() in Rust so clap errors go to stdout as JSONL