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
, andzap
emit 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.