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.