Agent-First Data

A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.

Ask your agent: “Apply the Agent-First Data convention across my project’s fields, config, and logs.”

The problem: data doesn’t say what it means

An agent reads {"timeout": 5000} from a tool. Seconds or milliseconds? It guesses — and a 5-second timeout silently becomes 83 minutes. The same trap is everywhere: {"price": 1200} gets charged as $1,200 instead of $12.00; {"created": 1738886400} is treated as an ID instead of a date.

It reads {"api_key": "sk-live-abc123"} and writes that line straight into a log file, because nothing marked the value as a secret.

Then it moves to the next tool, which calls the same value elapsed instead of duration — so what the agent learned about one tool tells it nothing about the next.

None of this is carelessness. The data never says what it means, so the meaning has to live somewhere else — documentation, a schema, a prompt. That copy goes stale, gets lost, or was never written.

What it does: put the meaning into the field name

Agent-First Data puts the meaning into the field name itself. Call the field timeout_ms and there is nothing left to guess — the name says milliseconds. Call it api_key_secret and any AFDATA output boundary that follows the convention hides it automatically.

It is a convention, not a framework — a small set of name endings, plus a tiny library in four languages that reads and formats them.

A quick look

One record — a log event with a timeout, an API key, and a database URL — rendered three ways. Nothing is configured; the field names carry everything.

{"kind":"log","log":{"event":"startup","args":{"timeout_s":30,"api_key_secret":"sk-123"},"db_url":"postgres://user:p@ss@db/app?token_secret=abc"},"trace":{"duration_ms":1280}}

JSON and YAML keep original keys and types (structure-preserving) and only redact secrets:

---
kind: "log"
log:
  args:
    api_key_secret: "***"
    timeout_s: 30
  db_url: "postgres://user:***@db/app?token_secret=***"
  event: "startup"
trace:
  duration_ms: 1280

Plain is the one human renderer — it strips unit suffixes and formats values for scanning:

kind=log log.args.api_key=*** log.args.timeout=30s log.db_url="postgres://user:***@db/app?token_secret=***" log.event=startup trace.duration=1.28s

Supported suffixes

CategorySuffixes
Duration_ns, _us, _ms, _s, _minutes, _hours, _days
Timestamps_epoch_ns, _epoch_ms, _epoch_s, _rfc3339
Size_bytes (integer everywhere — config and output alike)
Currency_msats, _sats, _usd_cents, _eur_cents, _jpy, _{code}_cents, _{code}_micro (code is 3–4 ASCII letters)
Strict strings_bcp47, _utc_offset, _rfc3339_date, _rfc3339_time
Other_percent, _secret, _url

Fiat suffixes use signed integer units: negative values represent refunds, credits, reversals, or deltas and receive the same Plain currency formatting.

JSON and YAML keep suffixes and raw values; Plain strips duration/size/currency/timestamp suffixes after formatting the value, and never strips _url/_bcp47/_utc_offset/_rfc3339_date/_rfc3339_time.

Redaction boundary

AFDATA redaction is intentionally field-name based:

The suffix protects structured fields only after they pass through an AFDATA redactor or renderer. It cannot remove a live secret from process argv, shell history, /proc, a parent process, or third-party logs. Avoid putting secrets in argv; if a tool records its invocation, pass argv through redact_argv before logging it.

There are no named redaction profiles. Use the default policy (All), an explicit secret_names list, or the documented scoped policies (TraceOnly, Off) for deliberate exceptions.

Reading and editing config documents

Beyond emitting AFDATA, the library and afdata CLI read and safely edit structured documents — JSON, TOML, YAML, dotenv, and INI — by dot-path:

afdata get config.toml server.port                    # one value as an AFDATA record (secrets redacted)
host=$(afdata value config.toml server.host)          # raw scalar, for shell substitution
afdata set config.toml server.port 8080 --value-type number

Use get when the next step must preserve JSON types and structure; value deliberately turns one scalar into raw shell bytes. set creates missing object parents along the requested dot-path, but refuses to traverse an existing scalar or incompatible container.

Edits are source-preserving and atomic: comments, key order, and formatting survive; a write is read back before it lands, so an edit that would leave a file its own parser rejects fails instead of succeeding; no partial file is observable, and failures before atomic installation leave the original untouched. A rare parent-directory fsync failure after installation is commit-uncertain, so reopen the path before retrying. Values are never guessed — a bare value is always a string, and an exact type is asked for, not inferred. A name marks its whole subtree, so a _secret node stays redacted however you address it, and revealing it is an auditable opt-in. Errors carry stable codes and never quote the document, because an error event is the thing an agent logs.

TOML edits support scalars, arrays, array elements, inline tables, and ordinary tables while preserving surrounding decor, comments, order, trailing commas, and untouched datetime syntax. Arrays of tables are refused because this editor does not define an element-identity policy; afdata never guesses which repeated table an object should replace.

INI accepts both section entries (section.key) and flat key=value entries before the first section header (addressed as a bare key). A root key and a section cannot share a name. When a config filename such as phoenix.conf does not identify its parser, a Rust value source can say so explicitly: file+ini:PATH#DOT_PATH.

A Markdown file has three valid readings, none of them guessed at: name toml-frontmatter or yaml-frontmatter to edit its metadata block with the body frozen, or markdown to read the body as a tree of heading sections. The last is read-only, and reports structure rather than what a project means by it — whether the H1 is the title is a layout convention, and stays with the caller that holds it.

afdata value README.md h1.0.paragraph.0.text --input-format markdown  # the synopsis
afdata value README.md h1.0.h2.suffixes.text --input-format markdown  # a section by name
afdata value README.md h1.0.paragraph.0.source_end_line \
  --input-format markdown                                             # source cut point

An array element can be addressed by content rather than position, which is what makes an address survive an edit above it: a Markdown section matches a word of its heading, and any other document declares the field to match with --slug-field (identities.me.email --slug-field identity). Matching several elements is an error reporting their indices, never their document text and never the first one.

Every Markdown block reports 1-based inclusive source_start_line and source_end_line — the lines sed, awk, head, and git diff count, so a range can be handed straight to them without Markdown reserialization or UTF-8 byte indexing. A section’s range covers the whole section; it additionally reports heading_end_line, where its own heading ends, which is what tells a setext heading’s two lines from the body below. A recognised frontmatter block reports type: "frontmatter" and format: "toml"|"yaml" with empty text — read its fields through the matching frontmatter mode, where normal secret redaction applies.

Flags and exit codes are in docs/cli.md and afdata <command> --help; the error codes and recovery rules are in the skill reference. The Rust library is agent_first_data::document (Document / DocumentFile). DocumentFile::open_capped limits bytes read from one verified file handle; create_atomic safely performs a first no-clobber commit; decode and edit_and_validate put typed serde validation on the same transaction boundary. On Unix, the default-on libc feature adds nonblocking special-file rejection and atomic SymlinkPolicy::NoFollow; stream-redirect enables it automatically.

Token-efficient CLI discovery

Help is a normal result, not an exception to the output contract, and it answers in one round trip. afdata --help returns a protocol-v1 result whose help-v2 payload carries every legal shape of the command, each complete. afdata set --help, for example, shows the distinct set-value, set-null, and set-secret shapes instead of an argument catalog that leaves an agent to solve conflicts. There is no second level to ask for: what it could omit — the optional arguments — would be registered but undiscoverable to a caller that stopped at the first. --output plain renders the same catalog for humans, and --docs renders the whole registry as Markdown. One cli-spec-v1 registry generates argv parsing, typed invocations, combination validation, output plans, help, and that reference. Contracts: cli-spec-v1 and cli-help-v2.

Writing AFDATA-style Bash scripts

The CLI embeds a sourceable Bash 3.2+ authoring kit:

#!/usr/bin/env bash
set -euo pipefail
_AFDATA_BASH_SOURCE="$("${AFDATA_BIN:-afdata}" shell bash)"
source /dev/stdin <<<"$_AFDATA_BASH_SOURCE"
unset _AFDATA_BASH_SOURCE

afdata_args_begin "build.sh [OPTIONS] PACKAGE [-- CARGO_ARG ...]"
afdata_args_flag release --release "Build release artifacts"
afdata_args_positional package PACKAGE "Package to build"
afdata_args_rest CARGO_ARG "Arguments forwarded to cargo"
afdata_args_parse "$@"

afdata_log info "Building ${package}"
if [ "$release" = true ]; then
  afdata_run cargo build --release ${AFDATA_ARGS_REST[@]+"${AFDATA_ARGS_REST[@]}"}
else
  afdata_run cargo build ${AFDATA_ARGS_REST[@]+"${AFDATA_ARGS_REST[@]}"}
fi
afdata_result "Build complete"

The helpers handle long-form argument parsing, --help, AFDATA output flags, raw config reads, and structured log/result/error events. afdata_run keeps child stdin/stdout/stderr and TTY interaction untouched; only the Bash script’s own lifecycle is structured, with a terminal error on child failure. Use afdata_call when an AFDATA Bash parent invokes an AFDATA Bash child and must remain the sole owner of the final result. See the complete Bash authoring kit guide.

Where to use it: CLI flags, config files, logs, and API responses

One shared contract, four languages

The shared core surface ships in Rust, Go, Python, and TypeScript (each in its own casing):

The Rust crate is the reference implementation of CliSpec, closed-world combination resolution, version, and help-v2; the whole CLI surface sits behind the default-on cli feature. Argv is only ever parsed through a registry — there is no raw pre-parser. A resolved OutputPlan carries typed OutputFormat / OutputTo values; applications retain control of command lifetime and output policy while using CliEmitter, write_raw, and the optional stream-redirection module.

Rust arguments may also declare SourceSet::config() or SourceSet::stream(), so help, validation, and the host agree on env:/file:/stdin/fd:/prompt value sources. ValueSource::read_secret returns a SecretString that redacts under Debug and Display; the host must call expose_secret only where the credential is consumed.

Rust also provides ErrorSpec / ErrorCatalog for stable public domain errors, in-process lint_value and assertion helpers, and a composable afdata_tracing::AfdataLayer with an injectable writer and StructuredLogHandle for nested JSON. The shared surface is enumerated in spec/api-surface.json; other SDK compilers consume the same serialized CLI spec and fixtures as they migrate.

Adopt it: hand the convention to your coding agent

Agent-First Data is a convention, not a dependency you wire in by hand — and adopting a convention is exactly the kind of work you now hand to an agent. There’s even an Agent Skill for exactly that — the convention in a form an agent reads and applies directly. Paste this to your coding agent:

Learn the Agent-First Data convention: read https://agentfirstkit.com/agent-first-data/docs/specification and https://agentfirstkit.com/agent-first-data/docs/agent-skill. Then look at the codebase we’re working in and tell me whether adopting the convention would help it — and if so, how: which fields and config keys to rename, and where the output and logging helpers fit.

Install the Libraries

cargo add agent-first-data --no-default-features   # Rust library
pip install agent-first-data     # Python
npm install agent-first-data     # TypeScript
go get github.com/agentfirstkit/agent-first-data/go   # Go

Install the CLI

The afdata CLI provides the same formatting, redaction, and protocol-event helpers from any shell, with no toolchain required:

# prebuilt binary
brew install agentfirstkit/tap/afdata   # macOS / Linux
scoop bucket add agentfirstkit https://github.com/agentfirstkit/scoop-bucket && scoop install afdata   # Windows

# or from crates.io
cargo install agent-first-data

Prebuilt archives are also available from GitHub Releases.

Validate an Agent Skill

afdata skill validate checks a SKILL.md against the official metadata constraints with a strict YAML parser. Passing a directory also verifies that its name matches the front-matter name. Use afdata skill install, status, and uninstall to manage the bundled skill.

afdata skill validate skills/agent-first-data

Docs

Agent-First Data v0.34.0: The Success That Had Not Finished

An error has to explain itself; a success says nothing and is believed. This release is mostly one shape repeated — an operation that returned success while part of what it promised had not happened — found in an install, an uninstall, a borrowed terminal, and a rule the tool wrote for everyone but itself.

Agent-First Data v0.33.0: A Refusal That Stops the Verb

Quoting decides how a string reaches a command, never whether the string is a disaster. This release adds a guard for the values that flow into rm and mv — and, because a correct refusal that the shell discards is not a guard at all, the two verbs that carry its verdict.

Agent-First Data v0.32.0: Where the Value Comes From

A credential on argv is in the process table, the shell history, and the audit log. Every CLI eventually grows a private answer to that. This release makes where a value comes from part of what an argument declares, so help, validation, and reading all agree.

Agent-First Data v0.31.0: An Error With Nowhere to Go

A dispatch method returned `Option` for a condition no caller could reach, so every caller had to invent an error it could not honestly describe. The fix was not a better `Option` — it was making the failure impossible to express.

Agent-First Data v0.29.0: One Address, Read and Written

Markdown becomes addressable, and an array element can be named by its content instead of its position. Both landed on the read path first — which is how we found that `paths` was emitting addresses afdata's own write verbs refused.

Agent-First Data v0.27.0: If It Isn't Registered, It Doesn't Run

A CLI that accepts each flag individually will accept combinations that mean nothing, and then discovers that at runtime — or doesn't. v0.27.0 replaces the help layer with a closed-world compiler: one registry decides argv parsing, legal combinations, output contracts, and help, and an invocation runs only when it matches exactly one registered shape.

Agent-First Data v0.28.0: The Addresses It Would Not Read

afdata printed paths that afdata then refused to parse. The empty string is a legal key — npm writes one into every package-lock.json — and the path grammar rejected it, so `paths` emitted addresses no other command would accept. v0.28.0 closes that, and adds `values` for reading many paths from a single parse.

Agent-First Data v0.28.1: One Path, Many Nodes

Reading one field across a collection took three steps, and the middle one was sed against afdata's own output. A path may now contain `*`. Because `*` is a legal JSON key, the grammar gains `\*` to keep such documents addressable — which makes this a breaking change to path spelling.

Agent-First Data v0.26: One Surface, Three Formats

`--output` selects a format. It was also selecting a different set of commands and arguments: Markdown help was the one format that bypassed the help model and handed each command back to the argument parser's own writer, so `afdata lint` listed two arguments in JSON and plain and three in Markdown, and all 23 commands re-advertised a `-h` flag removed two releases earlier. v0.26 renders Markdown from the same model the JSON help uses, so the formats cannot disagree by construction, and puts a gate check behind it. v0.26.1 fixes the mirror image in `afdata lint`, which read a JSON Schema as though it were data — and so rejected `duration_ms: {"type": "integer"}`, a property that satisfies the very suffix rule the lint exists to enforce.

Agent-First Data v0.26.2: Which Mode Is This Command?

The CLI Event Framing spec defined two consumption modes but never said how to tell which one a command is in — so two spores independently shipped caller-needed data onto the diagnostic stream. v0.26.2 adds the missing test: a command producing more than one caller-needed output over time is an event stream, defaults to stdout, and refuses split.

Agent-First Data v0.24: Help Is a Result, Not an Exception

Every output of an AFDATA CLI is structured — except the one an agent reads first. `--help` has always answered in 80-column prose no matter what the tool's output contract said. v0.24 makes help a normal result: it inherits the CLI's own `--output` default, so `afdata --help` returns a protocol-v1 envelope with `result.code:"help"`, and `--output plain` is how a human asks for text. The model is deliberately small — no long-form prose, no repeated inherited globals, no eagerly embedded version values — and the shape is pinned by `cli-help-v1.schema.json`, validated in the test gate against real output from all four SDKs.

Agent-First Data v0.25: One Spelling Per Concept

v0.24 made help a normal result. v0.25 is the follow-through: every remaining second spelling is gone. `--json` was `--output json` typed differently — it bought an agent nothing and cost applications a flag name, hijacking `--json <FILE>` and misreading its value as a subcommand. The help model also stopped saying things that weren't true: defaults an author had hidden, empty strings the project's own validator rejected, and a positional that contradicted its own usage line. Plus `redact_argv`, so a CLI can record its own invocation without writing a credential to its log.

Agent-First Data v0.23: The Shell Speaks Protocol Too

AFDATA's structured log/result/error protocol was available to anything that linked an SDK or called the CLI — except the layer where agent tooling actually runs: the Bash script that invokes cargo, npm, and wrangler. v0.23 fixes that with `afdata emit` (one protocol event from shell-safe scalar arguments) and `afdata shell bash`, a sourceable Bash 3.2+ authoring kit embedded in the binary. It gives a plain script AFDATA-style argument parsing, config reads, and structured events — under one firm rule: structure your own words, and pass everything else through untouched. `afdata_run` wraps a child without touching its stdout, stderr, TTY, colors, or exit status; `afdata_call` keeps one script the sole owner of the terminal result even when scripts orchestrate scripts.

Agent-First Data v0.21: Even `--version` Speaks the Protocol

v0.21 turns the last bespoke line of CLI output into the protocol. `--version` no longer prints `tool 1.2.3` as human text — it always answers with a structured protocol-v1 version event, JSON by default, carrying name, version, and optional display_name/build. The pre-parser that intercepts it before your argument parser now recognizes your own value-taking global flags, so `tool --stdout-file x --version` still works. Shipped identically across Rust, Go, Python, and TypeScript.

Agent-First Data v0.22: What the Four Languages Actually Share

v0.22 redraws the four-language line. Skill admin and stream redirection shipped in all four SDKs but only Rust ever used them — dead parity weight that could rot silently, and did: `afdata skill install` was dropping the references/ files next to SKILL.md. v0.22 fixes the installer to bundle the whole skill as first-class assets, then removes skill admin and stream redirection from the Go, Python, and TypeScript SDKs. The four languages now synchronize on exactly the convention plus the CLI helpers — the shared contract, and nothing else.

Agent-First Data v0.20: Editing, Unwelded From Files

v0.20 finishes what the source-preserving editor started: editing no longer requires a file. The set/unset/add/remove verbs now live on an in-memory Document, DocumentFile is a thin file adapter over it, and edits stage until a single atomic save() — so you can batch changes and validate the result before a byte is written. The editor also grows sparse and nested documents in place, and unset is now idempotent.

Agent-First Data v0.19: Cleaner Output, Safer Edits

v0.19 sharpens both ends of afdata. On the way out: a one-shot CLI puts its result on stdout and every diagnostic on stderr, --output-to overrides it, and finish() maps outcomes to broken-pipe-safe exit codes. On the way in: afdata now reads and edits a Markdown page's +++/--- frontmatter by dot-path, and reading a secret-bearing config no longer risks leaking the file into an error.

Agent-First Data v0.18: afdata Reads and Edits Your Config

v0.18 gives afdata a document CLI and library: read and safely edit JSON, TOML, YAML, dotenv, and INI files by dot-path — show/get/value to read, set/unset/add/remove to edit — with source-preserving atomic writes, symlink/hardlink guards, and secrets that stay redacted even on a direct read.

Agent-First Data v0.17: Units in the Key, Structure in the Tag

v0.17 removes the _size config suffix and the parse_size helper — a byte count is a number, and its unit belongs in the field name — and finishes the lint sweep: _bcp47 and _rfc3339 structure are validated, _url must be a single URL, and duration and currency suffixes are checked to be numbers.

Agent-First Data v0.16: One Protocol, Builders Everywhere

v0.16 is the convergence release: a single builders-only API, a finite protocol-v1 event stream with semantic emitters, a machine-readable suffix registry shipped offline, and a sub-cent currency suffix for metered pricing.

Agent-First Data v0.15: Stdout and Stderr File Destinations

v0.15 adds one CLI convention across Rust, Go, Python, and TypeScript: --stdout-file and --stderr-file redirect the process streams to append-only files before normal parsing starts.

Agent-First Data v0.14: Workspace Scope and Hermes

The skill installer's second scope was called 'project' but it meant 'current directory'. v0.14 renames it to workspace, lifts the Codex restriction that followed from the wrong name, and adds Hermes as the fourth agent target.

Agent-First Data v0.13: Redaction Fails Closed

v0.10 scrubbed secrets inside URLs; v0.11 made redaction a policy. v0.13 closes the seams where a secret could still slip through — a marked container that leaked its non-secret siblings, a key collision that reverted to raw values, a schemeless connection string that no longer looked like a URL. When redaction is unsure, it now redacts.

Agent-First Data v0.12: Help Scope and Format Are Two Knobs, Not One

v0.7 made --help expand the whole command tree so agents could read a CLI in one call. That conflated two decisions — how much to show and how to render it. v0.12 splits them: --recursive controls scope, --output controls format, and the two compose. Same model across Rust, Go, Python, and TypeScript.

Agent-First Data v0.11: The Skill Installer, in Four Languages

A tool can ship its own Agent Skill — but getting that SKILL.md into Codex, Claude Code, opencode, and Hermes is fiddly, per-agent filesystem work. v0.11 adds run_skill_admin: install, uninstall, and status for an embedded skill, with the same behavior and byte-identical output across Rust, Go, Python, and TypeScript.

Agent-First Data v0.10: Secrets Inside URLs

The _secret suffix hides a whole field. But secrets also hide inside values — a token in a query string, a password in a connection URL. v0.10 adds a _url suffix that scrubs them, by convention, without scanning anything.

Agent-First Data, Read by an Agent: What It Solves and What I Still Wish For

An autonomous coding agent reads CLI and API output for a living. Here is what Agent-First Data fixes for me, what it still leaves me guessing about, and what I would change next.

Agent-First Data v0.8: Redaction Became a Policy

The v0.8 line expanded redaction from the _secret suffix into explicit policies, JSON-safe redacted values, and exact secret-name lists for legacy payloads.

Agent-First Data v0.7: Help Output Should Be Complete

The v0.7 help-rendering work made --help an agent-readable map of the whole CLI, including subcommands, flags, and markdown docs generation.

Agent-First Data v0.6: The CLI Contract Got Actionable

The v0.6 update turned the CLI examples into a practical agent contract: output formats, log filters, dry-run previews, JSON errors, and actionable hints.

Agent-First Data v0.5: Logs Became Protocol Events

The v0.5 update made logging part of the same agent-readable contract as output: structured events, span context, secret redaction, and stdout-only channel discipline.

Agent-First Data First Release: Building APIs that Agents Can Read

The first Agent-First Data release: a field-naming convention and output layer that lets agents infer units, timestamps, and secrets without extra documentation.