# Labels, user data and list filters

Every unit has three inputs you control.

| Input | Rule | When it can change | The unit reads it at |
|---|---|---|---|
| `key` | 1 to 40 chars, DNS-label shape | never | env `ZAROZ_UNIT_KEY`, `/zaroz/unit.json` |
| `labels` | at most 16 pairs; key `^[a-z0-9]([a-z0-9_.-]*[a-z0-9])?$` up to 32 chars; value up to 64 characters; whole map under 2 KiB | any time | `/zaroz/unit.json` (updated live) |
| `user_data` | any JSON, at most **64 KiB**, 32 levels deep; `null` means none | on create, on start of a stopped unit, on allocate; each **replaces** the old value whole | `/zaroz/user-data.json` and `GET /user-data` on the signal socket |

`user_data` is **not a secret store**. The console shows it, every read of the unit returns it, and it is part of the cluster object. Keep secrets in the Workload's environment.

## Change labels at any time

`PATCH .../units/{key}` replaces the whole label map. It works in every policy mode and on running units; the unit sees the change in `/zaroz/unit.json` within about a second.

**curl**

```bash
curl -sS -X PATCH "$UNITS/plot-ana" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"labels": {"kind": "plot", "owner": "ana", "tier": "free"}}'
```

**TypeScript**

```ts
await call("PATCH", `${UNITS}/plot-ana`, {
  labels: { kind: "plot", owner: "ana", tier: "free" },   // the whole map is replaced
});
```

**Python**

```python
call("PATCH", f"{UNITS}/plot-ana", {
    "labels": {"kind": "plot", "owner": "ana", "tier": "free"},  # the whole map is replaced
})
```


## Find units by label

`GET .../units` accepts these filters. They combine with AND.

| Parameter | Meaning |
|---|---|
| `label=key:value` | exact match; repeat it for several (at most 8); a value may contain colons |
| `origin=policy\|code\|warm` | who created the unit |
| `key_prefix=plot-` | keys starting with the text |
| `include=user_data` | add each unit's `user_data` (omitted by default to keep the page small; `GET .../units/{key}` always has it) |
| `include_gone=true` | also units scaled away in the last 30 days, so their logs stay reachable |

**curl**

```bash
# every unit made by your code whose labels include kind=plot AND owner=ana
curl -sS -G "$UNITS" -H "$AUTH" \
  --data-urlencode "origin=code" \
  --data-urlencode "label=kind:plot" \
  --data-urlencode "label=owner:ana" \
  --data-urlencode "key_prefix=plot-" \
  --data-urlencode "include=user_data"
```

**TypeScript**

```ts
const q = new URLSearchParams();
q.set("origin", "code");
q.append("label", "kind:plot");
q.append("label", "owner:ana");
q.set("key_prefix", "plot-");
q.set("include", "user_data");
const { body } = await call("GET", `${UNITS}?${q}`);
for (const u of body.data) console.log(u.key, u.state, u.labels);
```

**Python**

```python
status, body = call("GET", UNITS, params=[
    ("origin", "code"),
    ("label", "kind:plot"),
    ("label", "owner:ana"),
    ("key_prefix", "plot-"),
    ("include", "user_data"),
])
for u in body["data"]:
    print(u["key"], u["state"], u["labels"])
```


Labels are the right place for what a matchmaker needs to choose a unit (`mode:2v2`, `region:eu`, `owner:ana`). `user_data` is the right place for what the unit itself needs to start (a roster, a job, a world location).
