Skip to main content

Define a label

Time to declare what "success" means. The sample dataset ships labels_repeat_purchase.csv — one row per user who placed an order:

entity_id,value,observed_at
u_00001,false,2025-05-05T09:53:00Z
u_00003,true,2025-03-18T14:02:00Z

value is true iff the user placed a second order within 30 days of their first; observed_at is when that outcome became knowable (first_order + 30 days). You define repeat_purchase_30d as a binary label reading that table, then upload the file to its own source:

04-define-label.sh
#!/usr/bin/env bash
# Quickstart step 4 — define the binary label and upload its observations.
set -euo pipefail

: "${TESSERA_API_BASE:?e.g. https://api.staging.tessera.zone/v1}"
: "${TESSERA_API_KEY:?your tsk_test_… key}"
: "${TESSERA_STATE_DIR:=.}"
AUTH=(-H "Authorization: Bearer $TESSERA_API_KEY")
SAMPLE_DIR="$TESSERA_STATE_DIR/sample"

# --- 1. a source for the label table -----------------------------------------
LABEL_SOURCE="$(curl -sS -X POST "$TESSERA_API_BASE/sources" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-label-source" \
-d '{"name": "quickstart-labels", "kind": "file_upload", "config": {}}')"
LABEL_SOURCE_ID="$(printf '%s' "$LABEL_SOURCE" | python3 -c 'import json,sys; print(json.load(sys.stdin)["source_id"])')"

# --- 2. the label definition --------------------------------------------------
# `observed_at` is when each outcome became knowable; `maturation_days: 7`
# masks any supervision where the entity was active within 7 days of the
# observation — the model must genuinely predict, never read back.
LABEL="$(curl -sS -X POST "$TESSERA_API_BASE/labels" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-label" \
-d '{
"name": "repeat_purchase_30d",
"entity_type": "user",
"task": "binary",
"source": {
"kind": "label_table",
"source_id": "'"$LABEL_SOURCE_ID"'",
"entity_id_column": "entity_id",
"value_column": "value",
"observed_at_column": "observed_at"
},
"maturation_days": 7
}')"
LABEL_ID="$(printf '%s' "$LABEL" | python3 -c 'import json,sys; print(json.load(sys.stdin)["label_id"])')"
printf '%s' "$LABEL" | python3 -c '
import json, sys
label = json.load(sys.stdin)
print(json.dumps({k: label[k] for k in ("label_id", "name", "entity_type", "task", "maturation_days")}, indent=2))
'

# --- 3. upload the observations to that source --------------------------------
SESSION="$(curl -sS -X POST "$TESSERA_API_BASE/uploads" "${AUTH[@]}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-upload-labels" \
-d '{"filename": "labels_repeat_purchase.csv", "format": "csv", "source_id": "'"$LABEL_SOURCE_ID"'"}')"
UPLOAD_ID="$(printf '%s' "$SESSION" | python3 -c 'import json,sys; print(json.load(sys.stdin)["upload_id"])')"
UPLOAD_URL="$(printf '%s' "$SESSION" | python3 -c 'import json,sys; print(json.load(sys.stdin)["upload_url"])')"
curl -sS -X PUT "$UPLOAD_URL" --data-binary "@$SAMPLE_DIR/labels_repeat_purchase.csv" > /dev/null
curl -sS -X POST "$TESSERA_API_BASE/uploads/$UPLOAD_ID:complete" "${AUTH[@]}" > /dev/null

LABEL_SYNC_ID="$(curl -sS "$TESSERA_API_BASE/uploads/$UPLOAD_ID" "${AUTH[@]}" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["sync_id"])')"
for _ in $(seq 1 300); do
STATUS="$(curl -sS "$TESSERA_API_BASE/syncs/$LABEL_SYNC_ID" "${AUTH[@]}" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')"
if [ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ]; then break; fi
sleep 1
done
echo "label sync: $STATUS"

# --- 4. sanity-check the ingested observations ---------------------------------
curl -sS "$TESSERA_API_BASE/labels/$LABEL_ID/stats" "${AUTH[@]}" | python3 -m json.tool
printf '%s' "$LABEL_ID" > "$TESSERA_STATE_DIR/label_id"

Download quickstart/04-define-label.sh — this exact file runs in CI on every change (hosts shown are for this environment).

Three deliberate choices in that definition:

  • kind: label_table — rows of the referenced source become label observations and are never normalized into events. Labels are targets, never inputs, structurally.
  • maturation_days: 7 — supervision only attaches where the user had been quiet for a week before the outcome was recorded; a model state from inside the outcome window would be grading itself on a half-known answer (masked, reason immature).
  • Unobserved stays unobserved — users without a row are masked, not assumed negative.

The label needs a registered entity type — user was auto-registered in step 1 when its first event streamed in, and the batch sync in step 2 independently ensures every mapped entity type is registered before its events commit. GET /labels/{id}/stats confirms what the builder will see: 1,700 observations, all matured, positive rate ≈ 0.40.

Next: build the dataset.