JSONL Is the Format AI Agents Were Missing
Today I finally understood why JSONL keeps showing up in agent tooling. I used it years ago for big data files and never thought much of it. This week I read Cloudflare’s post on orchestrating AI code review across thousands of merge requests, and the same line kept popping up: every agent process emits JSONL on stdout. That clicked. JSONL is not just a file format here. It is a response surface for unreliable processes.
The Problem With Plain JSON Here
Standard JSON wraps a dataset inside one array or object. A parser has to see the closing ] before it can give you a single record. For an agent orchestration job that runs up to 25 minutes across seven concurrent LLM sessions, that is a bad deal. If the process runs out of memory or crashes halfway, you get zero parseable output. Exactly when you need the debug logs, they are unparseable.
[
{ "event": "step_start", "agent": "coordinator", "ts": 1722816000 },
{ "event": "step_finish", "agent": "security", "tokens": 8421 },
/* process dies here, no closing bracket */
]
That whole blob is invalid JSON. The two records before the crash are gone.
JSONL Just Gives You the Records
JSONL (JSON Lines) drops the enclosing brackets. Each line is one complete, valid JSON object. Read a line, parse it, move on. No buffering the whole stream into memory. No waiting on a bracket that may never come.
{"event": "step_start", "agent": "coordinator", "ts": 1722816000}
{"event": "step_finish", "agent": "security", "tokens": 8421}
The process dies after line 2? You still have line 1 and line 2. Cloudflare pipes OpenCode with --format json so all stdout arrives as JSONL events, then buffers and flushes every 100 lines (or 50ms) to save disk from a stream of appendFileSync calls.
Why This Maps to Agents So Well
| Property | Plain JSON | JSONL |
|---|---|---|
| Crash-safe parsing | No, whole doc invalid | Yes, each line independent |
| Appendable | No, rewrite the file | Yes, just write a new line |
| Streamable mid-write | No, need the close | Yes, consumers read as you go |
| Split across workers | No fixed boundaries | Line breaks are valid split points |
| Corrupted record impact | Breaks the whole file | Skip the bad line, keep going |
| Diff-friendly | One blob, messy diffs | git diff works line by line |
This is exactly the shape of agent work: long-running, crash-prone, streaming, concurrent. You spawn sub-agents that may take minutes, may hit max_tokens, may hang for 60 seconds then die. You need a format where partial output is still output.
Where You Will Already See It
- Fine-tuning datasets for OpenAI, Anthropic, Gemini, Llama, and Mistral use JSONL. Each line is one training example: an instruction-response pair or a multi-turn conversation.
- Structured loggers like
pino,structlog, andzapemit one JSON object per line so aggregators can index entries without re-parsing the whole file. - OpenCode, Claude Code, and similar agent runtimes expose JSONL event streams on stdout so orchestrators can pull token usage, errors, and truncation signals in real time.
Reading a JSONL Stream in Python
The pattern stays the same regardless of language: read line by line, parse each line, never hold the whole file.
import json
with open("events.jsonl", "r") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue # skip corrupted line, keep the rest
yield event
That continue on a bad line is the whole point. One corrupt record does not sink the file.
What I Learned
- JSONL exists because plain JSON forces you to close the whole document before any record is parseable, which is the wrong trade for long-running and crash-prone processes.
- For agent orchestration, JSONL is not just storage. It is the streaming response surface between a parent process and several concurrent LLM sessions.
- Corrupt one line in JSONL and you lose one record. Corrupt one byte in a JSON array and you lose everything after it.
- Every major fine-tuning API ships datasets as JSONL because each training example is an independent unit, and append-and-go beats rewrite-the-array.
- If you are building anything that emits structured output from an LLM agent, start with JSONL. Re-inventing it is a tax, and every agent stack already speaks it.