Skip to content

Build a dataset from your sessions with labels

Turn recorded agent sessions into a queryable dataset by attaching labels that capture why the work happened, then filtering, intersecting, and exporting the labeled set.

Session records capture cost, turns, models, tool calls, and working directories on their own. A label adds the field the telemetry can’t infer: why the work exists. This guide walks from a naming convention to a labeled, exportable dataset. For the story behind the workflow, read Your Agent Work Is Already a Dataset.

Pick label names that answer questions

A label is free-form text, so spend the effort on the naming convention. The pattern that holds up is class:instance, where the class names the kind of intent:

decision:cassettes        an architecture decision and its downstream work
migration:tapes-cloud     a migration, implementation through cleanup
incident:pcc-1203         remediation and the follow-on work it created
experiment:prompt-cache   a bet, its cost, and its verdict
customer:acme             work a specific request triggered
skill:cassettes-docs      curation: sessions worth learning from later

One session can carry several labels. A session can be part of decision:cassettes and also flagged skill:cassettes-docs; the label sets stay independent, and you can query the intersection later.

Create labels and attach sessions

Create the label once. The display name is stored verbatim; identity is the folded key underneath, so Decision:Cassettes and decision:cassettes are the same label.

paperctl label create decision:cassettes

Attach it while the work is happening. label add takes one session id and any number of label names:

paperctl label add <session-id> decision:cassettes skill:cassettes-docs

The server returns at most one page per call (200 rows with --limit 200), so every pipeline below goes through a small helper that follows next_cursor to exhaustion. Define it once:

# list_all [sessions-list flags…] — emits one JSON object per session, all pages
list_all() {
  local cursor="" page
  while : ; do
    if [ -n "$cursor" ]; then
      page=$(paperctl sessions list "$@" --limit 200 --cursor "$cursor" --json)
    else
      page=$(paperctl sessions list "$@" --limit 200 --json)
    fi
    jq -c '.items[]' <<<"$page"
    cursor=$(jq -r '.next_cursor // empty' <<<"$page")
    [ -z "$cursor" ] && break
  done
}

Attaching after the fact works the same way, and it’s scriptable. To label everything a sessions list filter returns:

list_all --auth-subject <subject> --since 2026-08-16T00:00:00Z \
  | jq -r '.id' \
  | xargs -I{} paperctl label add {} decision:cassettes

Housekeeping is safe by design: label rename keeps every attachment, label rm detaches from one session without deleting the label, and label delete removes the label and all of its attachments everywhere.

Query the labeled set

sessions list --label filters server-side. The flag repeats, and repeated labels are ANDed:

# everything in the thread
list_all --label decision:cassettes

# the curated intersection: thread sessions also flagged for skills
list_all --label decision:cassettes --label skill:cassettes-docs

Each item carries the billing rollup (cost, turns, token counts, model mix), so many questions need nothing more than this step plus jq:

# total cost of the labeled thread
list_all --label decision:cassettes \
  | jq -s '[.[].rollup.usage.cost_usd] | add'

Export the full records

For anything deeper than rollups (per-turn spans, tool calls, prompts), export the labeled sessions as NDJSON. Export scopes to one session id at a time, so pipe the list through it:

mkdir -p dataset
list_all --label decision:cassettes \
  | jq -r '.id' \
  | xargs -I{} paperctl sessions export {} --detail spans --out dataset/{}.ndjson

--detail spans gives one span-level record per turn and is the default; --detail traces gives full trace and span granularity when you need every tool call and model span.

Two practical notes from running this at fleet scale. Keep the export parallelism low, two or three workers, because the export endpoint degrades under heavy parallel fetch. And expect a small number of sessions to refuse full export; for those, the list rollup still gives you accurate cost, turns, titles, and dates, and the label attachment itself survives, because labels live in session metadata rather than in exported content.

Ask the dataset questions

With rollups or exports on disk, the labeled set answers the questions the label class implies: cost and duration per intent, spend by person within a thread, the sessions that kept running after the project was “done,” the intersection of two intents. As a worked example, one decision:* thread in our own fleet resolved to 345 sessions and $4,728.15, which was 26.5% of a month’s spend, from a single filter once the set was known.

The labeled set is also an input. To turn your best sessions into a reusable skill, continue with Create a skill from labeled sessions.

Limits worth knowing

Labels are attached by people or scripts; nothing propagates them automatically today. Sessions that nobody labels stay invisible to label queries, which is why the attach-while-you-work habit, or a weekly retro-labeling pass, matters more than the tooling. A label records intent as declared, so it’s only as truthful as the person attaching it.

Frequently asked questions

Can a session carry more than one label?+
Yes. paperctl label add takes one session id and any number of label names, and the label sets stay independent. A session can belong to a decision thread and also be flagged for skill generation, and you can query the intersection with repeated --label flags, which are ANDed.
Can I label sessions after the work is done?+
Yes. Labels attach to existing sessions at any time, one session or a scripted batch at a time. Attaching in the same week the work happened is a few minutes of effort; reconstructing membership weeks later means reading prompts, which is the workflow labels exist to replace.
What happens to attachments when I rename or delete a label?+
Renaming keeps every attachment, because attachments follow the label by reference. Deleting a label removes it and every attachment it has, everywhere. Detaching from a single session with paperctl label rm never deletes the label itself.
Copied to clipboard