Machine Mode on the wire
Fourteen scenarios showing exactly what a Machine Mode tool writes to stdout, one JSON object per line.
Machine Mode is an open standard for how a command-line tool behaves when an AI agent runs it. Each scenario below runs one command, shows the stream it returns and notes what a consumer can conclude from it.
The tool is called outline throughout. It and its events are illustrative (hence the com.example schema names), written to show what a conforming implementation would emit.
Every conforming tool plays one of three pipeline roles: a source emits JSONL, a transformer reads JSONL and emits JSONL, and a sink reads JSONL and performs side effects. Most scenarios show sources; § 09 and § 13 add a transformer, and § 14 shows a sink. The shell tools you already use (jq, tee, set -o pipefail) handle JSONL as it is, so there's no new toolchain.
One stream, as it arrives
A search with a limit of 3, revealed at the pace a consumer reads it. The fourteen scenarios after it are static, so you can scan them.
$ outline search "agent operable" --output jsonl --limit 3
{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","tool_version":"1.8.2","aoi_version":"1.0-alpha","schema_name":"com.example.outline.events","schema_version":"1.0.0","command":"search"}
{"type":"hit","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"rank":1,"id":"doc_91","title":"Agent-operable interfaces","snippet":"A conforming tool exposes a stable interface…"}
{"type":"hit","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"rank":2,"id":"doc_177","title":"Why machine mode","snippet":"Today's tools were built for humans…"}
{"type":"hit","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"rank":3,"id":"doc_2041","title":"Adopt AOI-CLI","snippet":"The conformance contract is small…"}
{"type":"aoi:summary","aoi":{"seq":4,"run":"01JZQ7X4TQ9"},"ok":true,"count":3,"truncated":true,"next_cursor":"opaque-token-abc123","elapsed_ms":612}A consumer can learn what a tool exposes without running a real operation, without credentials and without network access. The schema and capabilities commands each return one JSON object, because this is discovery metadata rather than a JSONL stream.
$ outline schema --output json --command search{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Outline search event","type":"object","required":["type"],"properties":{"type":{"enum":["aoi:meta","hit","aoi:warning","aoi:error","aoi:summary"]}}}$ outline capabilities --output json{"tool":"outline","tool_version":"1.8.2","aoi_versions":["1.0-alpha"],"schemas":[{"name":"com.example.outline.events","versions":["1.0.0"],"default":"1.0.0"}],"commands":[{"name":"search","read_only":true,"bounded":true,"supports_cursor":true,"event_types":["aoi:meta","hit","aoi:summary"]},{"name":"delete","read_only":false,"destructive":true,"requires_confirm":true,"supports_dry_run":true,"supports_idempotency_key":true}]}
- Neither command needs credentials, network access or a real query.
capabilitiesis one JSON object, so a consumer can parse it in a single read.- The
destructiveflag ondeletetells a planner that this command needs extra ceremony.
Every read states how much it wants, and results arrive as the tool produces them. When more exists, the terminal summary says so and hands back a cursor for the next page.
$ outline search "agent operable" --output jsonl --limit 50{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","tool_version":"1.8.2","aoi_version":"1.0-alpha","schema_name":"com.example.outline.events","schema_version":"1.0.0","command":"search"}{"type":"hit","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"rank":1,"id":"doc_91","title":"Agent-operable interfaces","snippet":"A conforming tool exposes…"}{"type":"hit","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"rank":2,"id":"doc_177","title":"Why machine mode","snippet":"Today's tools were built for humans…"}…{"type":"hit","aoi":{"seq":50,"run":"01JZQ7X4TQ9"},"rank":50,"id":"doc_2041","title":"Adopt AOI-CLI","snippet":"The conformance contract is small…"}{"type":"aoi:summary","aoi":{"seq":51,"run":"01JZQ7X4TQ9"},"ok":true,"count":50,"truncated":true,"next_cursor":"opaque-token-abc123","elapsed_ms":612}
- The consumer chose the limit and the tool honored it.
truncated: truemeans more results exist beyond this page.next_cursoris opaque; its format belongs to the tool.
Pagination has one shape in every conforming tool instead of an ad hoc scheme per command. The cursor from one call goes back as --cursor on the next, and the consumer holds the state.
$ outline search "agent operable" --output jsonl --limit 50 --cursor opaque-token-abc123{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ8B2M1K"},"tool":"outline","aoi_version":"1.0-alpha","schema_name":"com.example.outline.events","schema_version":"1.0.0","command":"search"}{"type":"hit","aoi":{"seq":1,"run":"01JZQ8B2M1K"},"rank":51,"id":"doc_2110","title":"…"}…{"type":"hit","aoi":{"seq":33,"run":"01JZQ8B2M1K"},"rank":83,"id":"doc_3441","title":"…"}{"type":"aoi:summary","aoi":{"seq":34,"run":"01JZQ8B2M1K"},"ok":true,"count":33,"truncated":false,"next_cursor":null,"elapsed_ms":410}
truncated: falsewithnext_cursor: nullmarks the end of the result set.- A consumer never has to invent its own pagination.
Before a destructive operation runs, --dry-run emits one aoi:plan event for each side effect the real run would make, and makes none of them.
$ outline delete --where "stale=true" --dry-run --output jsonl{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","command":"delete","properties":{"dry_run":true}}{"type":"aoi:plan","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"action":"delete","target":"doc_44","title":"Old onboarding draft"}{"type":"aoi:plan","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"action":"delete","target":"doc_91","title":"2024-Q4 plan archive"}{"type":"aoi:plan","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"action":"delete","target":"doc_133","title":"Deprecated docs index"}{"type":"aoi:summary","aoi":{"seq":4,"run":"01JZQ7X4TQ9"},"ok":true,"properties":{"would_affect":3,"executed":false}}
properties.executed: falsemakes the no-op explicit, so a wrapper can assert on it.- Plan events are typed, just as the real apply events would be.
The real run requires --confirm. Bulk operations should also take --confirm-count N, which guards against a bigger blast radius than the caller expected, and every side effect that does happen produces a structured record.
$ outline delete --where "stale=true" --output jsonl --confirm --confirm-count 3{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ8C6N4D"},"tool":"outline","command":"delete"}{"type":"deleted","aoi":{"seq":1,"run":"01JZQ8C6N4D"},"target":"doc_44","at":"2026-07-30T17:42:11Z","actor":"kevin"}{"type":"deleted","aoi":{"seq":2,"run":"01JZQ8C6N4D"},"target":"doc_91","at":"2026-07-30T17:42:11Z","actor":"kevin"}{"type":"deleted","aoi":{"seq":3,"run":"01JZQ8C6N4D"},"target":"doc_133","at":"2026-07-30T17:42:11Z","actor":"kevin"}{"type":"aoi:summary","aoi":{"seq":4,"run":"01JZQ8C6N4D"},"ok":true,"count":3,"elapsed_ms":204,"properties":{"executed":true}}
- If
--confirm-counthad been 4, the tool would have refused without touching anything. deletedevents carry stable IDs, so a downstream audit log can ingest them as they are.
Send the same write twice and you get one outcome, with no duplicate. The idempotency key lets the tool recognize a retry and return the prior result instead of applying the effect again.
$ outline create-doc --title "Release notes" --idempotency-key rel-2026-05 --output jsonl{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"command":"create-doc"}{"type":"created","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"id":"doc_9001","title":"Release notes","idempotency_key":"rel-2026-05","duplicate":false}{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"ok":true,"count":1,"properties":{"executed":true}}$ outline create-doc --title "Release notes" --idempotency-key rel-2026-05 --output jsonl{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ8D7P3V"},"command":"create-doc"}{"type":"created","aoi":{"seq":1,"run":"01JZQ8D7P3V"},"id":"doc_9001","title":"Release notes","idempotency_key":"rel-2026-05","duplicate":true}{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ8D7P3V"},"ok":true,"count":1,"properties":{"executed":false}}
- Both runs return the same
idand exit 0. duplicate: trueon the second run is the explicit signal, so the consumer reads a field instead of guessing from elapsed time.
A tool reports its own health (config, credentials, dependencies and connectivity) as typed aoi:check events, and the summary rolls them up.
$ outline doctor --output jsonl{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","command":"doctor"}{"type":"aoi:check","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"name":"config_file","ok":true,"severity":"info","detail":"~/.outline.toml"}{"type":"aoi:check","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"name":"api_token","ok":true,"severity":"info","detail":"sourced from OUTLINE_TOKEN"}{"type":"aoi:check","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"name":"endpoint_reachable","ok":false,"severity":"error","detail":"connect ETIMEDOUT api.outline.example"}{"type":"aoi:summary","aoi":{"seq":4,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"failed","count":0,"error_count":0,"properties":{"checks_failed":1}}
- Every check has the same shape, which makes it easy for a monitoring agent to roll up.
aoi:summary.ok=falseplus a non-zero exit makes the failure unambiguous.
Some operations have no natural end. The tool documents that the stream is unbounded and may emit heartbeats so a consumer can tell a quiet stream from a dead one; it writes a summary only when it's cancelled.
$ outline watch --output jsonl{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","command":"watch","properties":{"unbounded":true}}{"type":"entry","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"change":"created","id":"doc_42","at":"2026-07-30T17:48:10Z"}{"type":"entry","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"change":"updated","id":"doc_42","at":"2026-07-30T17:49:23Z"}{"type":"aoi:heartbeat","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"at":"2026-07-30T17:50:00Z"}{"type":"aoi:heartbeat","aoi":{"seq":4,"run":"01JZQ7X4TQ9"},"at":"2026-07-30T17:51:00Z"}{"type":"entry","aoi":{"seq":5,"run":"01JZQ7X4TQ9"},"change":"deleted","id":"doc_19","at":"2026-07-30T17:51:42Z"}⌃C{"type":"aoi:summary","aoi":{"seq":6,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"cancelled","count":3}
properties.unbounded: trueon meta tells the consumer not to wait for natural completion.- Heartbeats let a supervisor tell a quiet stream from a dead one without relying on TCP keepalive.
- A graceful interrupt still produces a structured summary, with
reason: "cancelled".
The output of one Machine Mode tool is the input of the next. A transformer consumes the upstream framework events and emits its own, so the end of the pipe sees one meta and one summary however many stages the chain has.
$ outline search "agent" --output jsonl --limit 20 \$ | summarize --input-jsonl - --output jsonl \$ | tee run.jsonl \$ | jq -e 'select(.type=="aoi:summary") | .ok == true'{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"summarize","tool_version":"0.4.1","aoi_version":"1.0-alpha","schema_name":"com.example.summarize.events","schema_version":"1.0.0","command":"summarize"}{"type":"abstract","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"source_id":"doc_91","text":"A typed interface…"}…{"type":"aoi:summary","aoi":{"seq":21,"run":"01JZQ7X4TQ9"},"ok":true,"event_count":22,"count":20,"input_count":20,"warning_count":0,"error_count":0,"upstream":{"tool":"outline","tool_version":"1.8.2","schema_name":"com.example.outline.events","schema_version":"1.0.0"}}
- Exactly one
aoi:metaand oneaoi:summaryreach the end of the pipe, and both are the transformer's. Per § 13.1, an AOI producer never forwards upstreamaoi:metaoraoi:summary. - Upstream identity rides on
aoi:summary.upstream: one object, direct upstream only. It can't live onaoi:meta, which has to go out before the transformer has read the upstream's own meta. - That invariant keeps EOF-without-summary a reliable crash signal, because a summary in the stream always means the last stage finished.
jq -eon the terminal summary turns Machine Mode into a shell-level boolean, and withset -o pipefailan upstream failure also fails the pipeline's exit status.
One binary can ship more than one version of its event schema. Each consumer pins the version it supports, and the tool honors the request and reports which version it used.
$ outline search "agent" --output jsonl --schema-version 1 --limit 1{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","schema_name":"com.example.outline.events","schema_version":"1.0.0"}{"type":"hit","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"rank":1,"id":"doc_91","title":"Agent-operable"}{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"ok":true,"count":1}$ outline search "agent" --output jsonl --schema-version 2 --limit 1{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ8F9H6N"},"tool":"outline","schema_name":"com.example.outline.events","schema_version":"2.0.0"}{"type":"hit","aoi":{"seq":1,"run":"01JZQ8F9H6N"},"rank":1,"document":{"id":"doc_91","title":"Agent-operable","mime":"text/markdown"}}{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ8F9H6N"},"ok":true,"count":1}
- v2 nested the result in a
documentobject. That's a breaking shape change, so it got a major bump. - A consumer built on v1 keeps working; one that wants v2 opts in explicitly.
An aoi:error carries category and retryable as portable fields, so a consumer can write its retry logic once and use it with every conforming tool, without learning each tool's own codes.
$ outline create-doc --title "…" --output jsonl{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"command":"create-doc"}{"type":"aoi:error","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"code":"RATE_LIMITED","category":"rate_limited","message":"60 writes per minute exceeded","retryable":true,"retry_after_ms":7200}{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"failed","error_count":1,"properties":{"executed":false}}
category: rate_limitedcomes from the standard taxonomy, so a consumer can route the error without reading the tool's docs;retryabledecides whether to retry at all.retry_after_msis a minimum delay, not a schedule. The consumer waits at least that long, adds jitter and applies its own attempt limit.
Exit 0 means the process finished. Success means the work completed and was reported, and without a terminal summary, EOF is the cross-language crash signal even when the exit code is 0.
# Healthy run: exit 0, summary present, ok=true{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"command":"search"}{"type":"hit","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"rank":1,"id":"doc_1"}{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"ok":true,"count":1}$ echo $? # 0# Crash mid-stream: exit 0 anyway, because a top-level handler swallowed the exception{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ8H3K8T"},"command":"search"}{"type":"hit","aoi":{"seq":1,"run":"01JZQ8H3K8T"},"rank":1,"id":"doc_1"}# <EOF, no summary, exit 0>$ echo $? # 0, but the consumer MUST treat this as failure
- A consumer that trusts the exit code alone will accept the truncated stream as success without noticing.
- The terminal summary is the only honest success signal, and its absence is the only honest crash signal.
A transformer reads upstream AOI events on stdin, dispatches on event.type and emits its own typed events. The same envelope rules apply: aoi:meta opens the stream, aoi:summary ends it, and domain events get transformed in between. Upstream aoi:meta and aoi:summary are consumed and never forwarded (§ 13.1), so the stream at the end of the pipe still has exactly one of each.
$ outline search "agent operable" --output jsonl --limit 3 | summarize --input-jsonl - --output jsonl --max-tokens 60{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"summarize","tool_version":"0.4.1","aoi_version":"1.0-alpha","schema_name":"com.example.summarize.events","schema_version":"1.0.0","command":"summarize","properties":{"input_schemas":["com.example.outline.events@1.0.0"]}}{"type":"abstract","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"source_id":"doc_91","text":"A typed interface a non-human can rely on — discoverable, bounded, safe."}{"type":"abstract","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"source_id":"doc_177","text":"Today's tools were built for humans; agents need something safer than scraped terminal text."}{"type":"abstract","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"source_id":"doc_2041","text":"Conformance is small: emit JSONL, expose a schema, terminate on a summary."}{"type":"aoi:summary","aoi":{"seq":4,"run":"01JZQ7X4TQ9"},"ok":true,"count":3,"input_count":3,"warning_count":0,"error_count":0,"elapsed_ms":380}
properties.input_schemasin meta declares which upstream shapes the transformer accepts, so a downstream tool can check that the chain is type-compatible.source_idpreserves the upstream identity, so a consumer can match transformed events back to the originals.- Unknown unprefixed upstream event types are always safe to ignore. An unknown
aoi:type may be ignored too, but then the transformer can't report success (§ 13). - An upstream
aoi:errorsetsaoi:summary.ok=falseunless--continue-on-erroris passed, and its retry fields are copied intoaoi:summary.caused_bysoretry_after_mssurvives the pipe.
A sink reads JSONL on stdin and performs side effects. Most of its output is the audit trail of what it created, updated or sent, wrapped in the meta and summary events every conforming tool emits, and it can sit at the end of an AOI pipeline.
$ cat exports.jsonl | outline import --input-jsonl - --output jsonl --confirm{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","command":"import","properties":{"input_mode":"jsonl","input_schemas":["com.example.export.events@1.0.0"]}}{"type":"created","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"target":"doc_9001","title":"Q4 retrospective","source_id":"export_44","at":"2026-07-30T18:11:02Z"}{"type":"created","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"target":"doc_9002","title":"Postmortem template","source_id":"export_45","at":"2026-07-30T18:11:03Z"}{"type":"aoi:error","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"scope":"item","category":"validation","code":"INPUT_JSONL_PARSE_ERROR","line_number":7,"message":"Invalid JSON on input line 7","retryable":false}{"type":"aoi:summary","aoi":{"seq":4,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"failed","count":2,"input_count":3,"error_count":1,"partial":true,"elapsed_ms":612,"properties":{"executed":true}}
- A sink still emits meta and summary; the AOI envelope is the same for every role.
createdevents carry the new resource ID and thesource_idfrom the upstream input, so the consumer can build a forward-mapping audit log.- The malformed line produces an item-level error (
scope: "item", pointing at itsline_number) and the run carries on, which is the per-line tolerant mode from § 12.2. A tool that works this way has to document it, and for mutating imports the spec recommends failing fast instead. partial: truetogether withproperties.executed: truesays the work happened and some inputs failed.
Build a tool that does this
The conformance contract is small: emit JSONL, expose a schema, end on a summary, handle pipes cleanly and redact secrets. The full checklist is in the specification.