# Smart Tags

> Virtual tags derived from tagging policies you define — each policy enforces a tag key and builds its value from the resource's own fields. Applied for cost attribution only, never written back to the cloud; reviewable and reconciled on every refresh.

Source: https://zop.dev/developer-docs/operations/smart-tags

---

Smart tags are **virtual tags** ZopNight derives from **tagging policies** you define. Each policy enforces a tag key and derives its value from the resource's own fields, so the result is reproducible and cheap to re-run. Smart tags are applied **inside ZopNight for cost attribution only**; they are never written back to the cloud provider.

**Info**

Tag-based cost attribution and reporting rely on consistent tags. In practice most accounts have partial coverage — smart tags close the gap so [Showback](https://zop.dev/docs/operations/showback) and [Reports](https://zop.dev/docs/operations/reports) can attribute cost even on resources the cloud account owner never tagged.

## How derivation works

1. You create a **tagging policy**: an optional `selector` (which resources it applies to — by provider, type, region, …) plus a `tag` spec (the key, and the value built from resource fields).
2. The recommender resolves each resource against its policies. For a match, it derives the value: `fields → join → standardize` (e.g. `res.provider` + `res.region` joined by `-`, lowercased → `gcp-us-central1`).
3. Derived tags land as **pending**. They are never auto-applied to cost — you **accept** the ones you want (or **revoke** later).
4. `pending = derived − accepted`. There is no "rejected" state.

Value source fields are restricted to stable resource attributes — `res.type`, `res.region`, `res.provider`, `res.instance_type`, `res.name`. Empty parts are skipped so a partially-absent multi-field value never produces separator garbage; an all-empty value omits the tag (there is no literal fallback).

## Lifecycle & reconciliation

Tags are re-derived per resource on each `resource.discovered` event and across the org on a full refresh (`POST /recommendations/refresh`). Every re-derivation **reconciles** your accepted tags: an accepted tag is kept only if it is still derived *with the same value*, otherwise it is dropped — so disabling a policy or changing its value automatically removes the now-unsourced tag on the next refresh. When a resource is deleted in the cloud, its smart-tag row is removed.

## List Smart Tags

### GET /smart-tags

Paginated per-resource derived/accepted tags for the current org, plus org-wide pending/accepted counts for the tab badges.

#### Query Parameters

| Parameter | Description |
|---|---|
| `status` | Lifecycle filter: pending \| accepted. Omit for all. |
| `provider` | Cloud provider filter: aws \| gcp \| azure. |
| `page` | Page number (default: 1). |
| `size` | Items per page (default: 20, max: 100). |

```json title="Response"
{
  "data": {
    "items": [
      {
        "resourceUid": "//artifactregistry.googleapis.com/projects/acme/locations/us-central1/repositories/web",
        "resourceName": "web",
        "resourceType": "artifact-registry",
        "provider": "gcp",
        "cloudAccountId": "acc_1",
        "derived":  { "cost-allocation": "gcp-us-central1", "cloud-platform": "gcp" },
        "accepted": { "cloud-platform": "gcp" },
        "pending":  { "cost-allocation": "gcp-us-central1" }
      }
    ],
    "hasMore": false,
    "limit": 20,
    "counts": { "pending": 132, "accepted": 6 }
  }
}
```

## Accept or Revoke

### PATCH /smart-tags

Promote pending tags into cost attribution (accept) or remove them (revoke). Body-based so resourceUid may contain slashes (e.g. GCP //... paths).

Send `accepted: true` to accept or `accepted: false` to revoke. Name the keys with `keys: [...]`, or pass `all: true` (accept only) to act on every derived key. A mutation with no keys and `all` unset is rejected — it would otherwise silently no-op.

```bash title="Accept specific keys"
curl -X PATCH https://zopnight.com/api/smart-tags \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "resourceUid": "//artifactregistry.googleapis.com/projects/acme/locations/us-central1/repositories/web",
    "keys": ["cost-allocation"],
    "accepted": true
  }'
```

```bash title="Accept every derived key on a resource"
curl -X PATCH https://zopnight.com/api/smart-tags \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "resourceUid": "i-0abc123def456", "all": true, "accepted": true }'
```

```bash title="Revoke (back to pending)"
curl -X PATCH https://zopnight.com/api/smart-tags \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "resourceUid": "i-0abc123def456",
    "keys": ["cost-allocation"],
    "accepted": false
  }'
```

## Tagging Policies

Tagging policies are stored in the generic policy store and managed through the standard policy CRUD endpoints with `domain: "tagging"`. The recommender serves the form vocabulary and validates a spec before you create it.

### POST /policies

Create a tagging policy (domain=tagging).

```bash title="Request"
curl -X POST https://zopnight.com/api/policies \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "tagging",
    "targetType": "organisation",
    "name": "cost-allocation",
    "spec": {
      "selector": { "all": [{ "attr": "res.provider", "op": "eq", "values": ["gcp"] }] },
      "tag": {
        "key": "cost-allocation",
        "value": { "fields": ["res.provider", "res.region"], "join": "-", "standardize": true }
      }
    },
    "enabled": true
  }'
```

### GET /tagging/policies/catalog

Form vocabulary: selectable condition facets, operators, and the allowlisted value-source fields.

### POST /tagging/policies/validations

Validate a tagging spec before create. Returns { valid, errors[] }.

**Info**

The `selector` is a boolean tree (`all` / `any` / `not` over `{attr, op, values}` leaves; ops `eq`, `neq`, `in`, `contains`, `regex`, `exists`) — string ops match case-insensitively. The `value.join` separator is limited to `-` or `_` (safe across AWS, GCP and Azure labels).
