Skip to content

JSONL Is the Format AI Agents Were Missing

3 min read

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

PropertyPlain JSONJSONL
Crash-safe parsingNo, whole doc invalidYes, each line independent
AppendableNo, rewrite the fileYes, just write a new line
Streamable mid-writeNo, need the closeYes, consumers read as you go
Split across workersNo fixed boundariesLine breaks are valid split points
Corrupted record impactBreaks the whole fileSkip the bad line, keep going
Diff-friendlyOne blob, messy diffsgit 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, and zap 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.

References