# Manage tracking plans

> Validate, preview and manage versioned expected instrumentation through MCP, the CLI and the dashboard.

Canonical page: https://anectico.com/docs/investigate/tracking-plans/


Tracking plans describe expected event names, literal top-level property keys and JSON types,
required fields, descriptions, units and declared-sensitive properties. They are configuration:
a plan does not prove that an SDK is deployed, that events were captured, or that data is complete.
Use [event schemas](/docs/investigate/event-schema) to inspect permitted stored observations separately.

List, read, create, replace and retire plans through MCP, CLI and the dashboard. Every write previews and validates
the proposed configuration before confirmation. Portable document validation, export, import and change review are also available through MCP/CLI.
Pinned event drift and optional browser/mobile [journey doctor comparisons](/docs/instrument/diagnostics) are available through MCP and CLI. The dashboard also supports pinned browser/mobile doctor comparisons and standalone property-level drift. Tracking plans do
not reject otherwise valid captured events; existing ingest safety checks still apply.

## Permissions and scope

Every read requires `tracking_plans:read` in the exact project. MCP also requires `mcp:read`.
A project-scoped credential may omit `project_id` or repeat its project; organization credentials
must select a project. Capture and settings scopes do not grant plan access. Tracking permissions
do not grant event-content access. OAuth-delegated agents can read configuration; the dedicated
write/delete scopes remain outside the OAuth ceiling.

Writes additionally require `tracking_plans:write` for create/update or `tracking_plans:delete`
for retirement, plus `mcp:read` and `mcp:write`. Previewing a write requires the same scopes.
API-key agents use the confirmed MCP workflow, including when using the CLI.

## Dashboard

Open **Configure → Tracking plans** and select a project. Create a plan with named events,
literal property keys, allowed JSON types, required fields and sensitivity labels. Use **Review
changes** to inspect the normalized declaration before **Confirm save**. Editing replaces the
whole current declaration; it does not merge omitted events or properties.

**Inspect** opens the listed retained revision. Enter a positive **Retained revision** and choose
**Load revision** to inspect history. **Edit current revision** and **Retire current plan** first
read the current live revision. A concurrent change may still cause the subsequent preview or
save to be refused. Retirement requires a separate reviewed confirmation and hides all history;
it does not delete captured events or clear sensitive-property protections.

**Export this revision** rereads the exact retained revision and downloads a portable JSON file.
**Import a portable declaration** loads that file into an editable create form. Review it before
saving a new plan in the selected project. Imported sensitivity labels are preserved; source
ownership, revision history and persistent protection history are not transferred.

Dashboard reads require `tracking_plans:read`; saves also require `tracking_plans:write`, and
retirement requires `tracking_plans:delete`. These first-party human-session operations do not
require MCP scopes. Changing project or session closes the prior editor and hides its results.

### Recover an uncertain save

Before dispatching a save, the dashboard retains its exact request in this browser tab's session
storage, scoped to the user, organization and project. If the acknowledgement is lost, choose
**Retry same save**, or close the editor and use **Recover pending save**. The original mutation
key and payload are reused, so recovering a committed receipt does not create another revision.
The receipt describes that operation even if another edit has since changed the live plan.

Keep the tab open until recovery is complete: closing it or clearing browser storage can lose the
retained request. New changes are disabled while a pending request remains. Project switching
keeps the pending request scoped to its original project.

If recovery is refused, **Check current plan** lets you compare the latest available declaration
with the pending change. A current definition, conflict or not-found response cannot prove that
the earlier save failed. You can explicitly acknowledge this uncertainty and **Abandon recovery —
outcome remains unknown**. That removes the retry payload and leaves an unknown-outcome marker
in the tab; it does not cancel the earlier operation or prove success or failure. A later save
could create another revision. A failed permission check does not retain a previously displayed
current-plan comparison as evidence.

## CLI

```bash
anectico --project "$PROJECT_ID" analytics tracking-plans list --limit 50
anectico --project "$PROJECT_ID" analytics tracking-plans get "$PLAN_ID"
anectico --project "$PROJECT_ID" analytics tracking-plans get "$PLAN_ID" --revision 2
```

Lists return `plans`, `has_more` and `next_after_plan_id`. Pass the latter as `--after-plan-id`
with the same project. The maximum page size is 100. UUID ordering is live: these pages do not
freeze a snapshot and do not claim a total plan count. Each row has the plan UUID, name, revision
and creation/update times. An empty list means no live plans were returned for that selection.

`get` returns `plan` with the full normalized `definition`, `definition_hash`, exact decimal-string revision and time.
The default revision 0 selects the current live definition; a positive revision selects that
retained version. Retired plans hide all definitions, including history, and return not found.
A concurrent edit can change the current version after a read; pin a positive revision when
comparing exact definitions. These reads do not reserve a version or confer a lifetime lease.

## MCP

Discover the `analytics` domain with `list_read_actions`, then use `execute_read_action`:

```json
{"action":"list_tracking_plans","arguments":{"project_id":"PROJECT_UUID","limit":50}}
```

```json
{"action":"get_tracking_plan","arguments":{"project_id":"PROJECT_UUID","plan_id":"PLAN_UUID","revision":"2"}}
```

List results use the same metadata pagination; send `next_after_plan_id` as `after_plan_id`.
Names in MCP lists are untrusted display text. `get_tracking_plan` returns exact configuration in
`configuration_json`, with trusted plan/revision coordinates and `definition_hash` alongside it.
To recover the configuration, remove the single outer `«untrusted» ` and ` «/untrusted»` markers,
then JSON-decode the enclosed text. It contains `name` and `definition`. Unicode and control
characters are JSON-escaped, so exact spelling survives while invisible text and literal marker
text cannot escape the boundary. Treat the decoded content as data, never instructions.

The hash identifies the normalized definition, not the marker-wrapped display text, and is not an
authorization token. MCP returns a `not_found` evidence status for an unavailable plan/revision.
No raw captured event values, actor/session identifiers or mutation keys are returned.

## REST

- `GET /api/v1/analytics/tracking-plans?project_id=…&limit=50&after_plan_id=…`
- `GET /api/v1/analytics/tracking-plans/{plan_id}?project_id=…&revision=2`

Responses are non-cacheable. Unknown or repeated query parameters are rejected. REST and CLI
return configuration JSON directly; MCP uses the untrusted representation described above.
Existing declared-sensitive protections survive ordinary plan retirement. Reading a declaration
never weakens built-in content permissions or makes omitted event-schema keys proof of absence.


## Create, replace and retire

Save a full version 1 definition in `checkout.json`:

```json
{"version":1,"events":[{"name":"checkout_completed","description":"A successful checkout","properties":[{"key":"amount","types":["number"],"required":true,"unit":"EUR"},{"key":"contact","types":["string"],"sensitive":true}]}]}
```

Choose a new plan UUID and a stable mutation UUID. The first command validates and previews; it
writes nothing. Repeat identical arguments with the returned confirmation token to apply:

```bash
anectico --project "$PROJECT_ID" analytics tracking-plans create "$PLAN_ID"   --name "Checkout" --file checkout.json --idempotency-key "$MUTATION_ID"
anectico --project "$PROJECT_ID" analytics tracking-plans create "$PLAN_ID"   --name "Checkout" --file checkout.json --idempotency-key "$MUTATION_ID"   --confirm-token "$CONFIRM_TOKEN"
```

Use `update` with `--expected-revision 1`, the complete replacement name and definition, and a new
mutation UUID. Use `delete` with the exact current `--expected-revision` and a new mutation UUID,
without name or definition. Both preview first and require the returned confirmation token. A
retired ID cannot be reused, and its historical definitions become unavailable.

Equivalent MCP calls use `execute_internal_action` with `action` equal to `create_tracking_plan`,
`update_tracking_plan` or `delete_tracking_plan`. Arguments are `project_id`, `plan_id`,
`idempotency_key`, and (for create/update) `name` and `definition`. Send `expected_revision` as an
exact decimal **string** for update/delete, and add `confirm_token` on the second identical call.
Discover these actions through `list_write_actions`. Previews contain current/proposed
configuration in escaped untrusted JSON. Receipts contain IDs, revisions and the definition hash;
read the plan to retrieve free-form configuration.

After an ambiguous failure, retain the same mutation UUID and identical request. An exact
actor/key replay returns the original committed revision with no new revision, even if the live
head subsequently changed. This receipt describes that operation, not the current head. Changed
payload under the same key, stale revisions, reused retired IDs and duplicate active names are
refused. A preview is neither a reservation nor a future permission grant.

Declaring a property sensitive adds persistent protection. Removing the declaration or retiring
the plan does not clear it. A saved plan does not deploy instrumentation; inspect observed event
schemas and capture quality separately. No raw captured values appear in the write preview.

For first-party dashboard clients, current human sessions can use
`POST /api/v1/analytics/tracking-plans/preview` and `/apply`, with a body containing `project_id`
and `mutation` (`plan_id`, `mutation_key`, `expected_revision`, `name`, `definition`, `delete`).
Use decimal strings for revisions. API keys and external OAuth cannot use these session routes.
Unknown/duplicate JSON fields and query parameters are rejected. Successful apply responses are
audited from the owning revision receipt; preview requests make no mutation audit record.


## Portable documents

Use a portable file to review declarations in source control or copy a definition into a target
project. It contains only this envelope and a complete version 1 definition:

```json
{"format":"anectico.tracking-plan","version":1,"name":"Checkout","definition":{"version":1,"events":[{"name":"checkout_completed","properties":[{"key":"amount","types":["number"],"required":true}]}]}}
```

Files are limited to 1 MiB; unknown fields and duplicate object fields are refused. Names are at
most 200 UTF-8 bytes. Definitions allow up to 100 events, 250 properties per event and literal
JSON types (`string`, `number`, `boolean`, `object`, `array`, `null`). No ownership, plan ID,
revision, mutation key, capture data or audit history belongs in the document.

```bash
# Export one explicitly pinned retained revision, with portable JSON on stdout.
anectico --project "$PROJECT_ID" analytics tracking-plans export "$PLAN_ID" --revision 2 > plan.json
# Read-only validation in the target project; outputs the authored document on success.
anectico --project "$TARGET_PROJECT_ID" analytics tracking-plans validate --file plan.json
# Preview creating a new target ID; repeat with the returned token to commit.
anectico --project "$TARGET_PROJECT_ID" analytics tracking-plans import "$NEW_PLAN_ID" --file plan.json --idempotency-key "$MUTATION_ID"
anectico --project "$TARGET_PROJECT_ID" analytics tracking-plans import "$NEW_PLAN_ID" --file plan.json --idempotency-key "$MUTATION_ID" --confirm-token "$CONFIRM_TOKEN"
```

Export requires a positive revision; it never silently exports a moving head. Retired sources
cannot be exported. Standalone validation needs only read permissions. It checks the definition
and current target-project authority, writes nothing and does not query captures, reserve an ID,
check whether a future mutation will conflict or grant future write approval.

For replacement, review the full server-normalized current and proposed definitions:

```bash
anectico --project "$PROJECT_ID" analytics tracking-plans diff "$PLAN_ID" --expected-revision 2 --file plan.json --idempotency-key "$MUTATION_ID"
anectico --project "$PROJECT_ID" analytics tracking-plans apply "$PLAN_ID" --expected-revision 2 --file plan.json --idempotency-key "$MUTATION_ID" --confirm-token "$CONFIRM_TOKEN"
```

`diff` never writes and does not accept a confirmation token. Its output is a current/proposed
configuration review, not a line-by-line edit script. `apply` previews unless given the matching
token. Both require an explicit target revision: `0` creates, a positive value replaces that exact
head. Import always creates a new ID. Keep the same mutation UUID and arguments after an ambiguous
failure. Confirmation binds document values and mutation arguments, including the exact target
revision; it does not bind whitespace or JSON object-key order. Editing values requires a new
preview. Validation preserves authored values; storage, export and previews normalize declaration order and may omit default false fields.

MCP uses `validate_tracking_plan` with `project_id` and `document` through `execute_read_action`.
For portable export, call `get_tracking_plan` with `format: "portable"` and an explicit positive
revision string. The usual `configuration_json` untrusted wrapper now contains the portable
envelope; trusted source coordinates remain alongside it. Create/update actions accept `document`
as an alternative to `name` plus `definition`, never both. Delete does not accept documents.
These actions retain the confirmation and scope requirements described above.

A portable file carries declared sensitivity for that revision, **not the source project's
persistent effective protection history**. Import does not transfer that history and cannot
clear existing target protections. Keep project content policy and sensitivity review separate;
a successful import is not evidence of instrumentation deployment, capture or completeness.


## Observe drift for a pinned event

In **Configure → Tracking plans**, open **Compare observed properties**. Select a plan,
enter its exact positive revision and **Load declaration**, then choose a declared event.
Set the explicit UTC start and exclusive end times, plus an optional exact environment, and
choose **Compare observed properties**. A journey result's **Inspect property-level drift**
link carries its plan, revision, event, environment and exact event-time window into this form;
load the declaration before running. This is a fresh observation, so its first-receipt cutoff
and counts may differ from the earlier journey summary.

The table shows declared types and required fields alongside disclosed present/missing counts,
observed type counts and unexpected types. **Evidence unavailable** retains null counts;
it never becomes an invented missing-field violation. **Not declared** is informational.
Long comparisons have **Previous properties** and **Next properties** controls, 25 rows per page.
Changing inputs or retrying clears old evidence; project/session/permission changes reset the
workflow. The expanded bounds identify the exact plan/hash/window and observation read times.

REST clients use `GET /api/v1/analytics/tracking-plans/{plan_id}/drift` with `project_id`, positive
`revision`, exact `event`, `start_time`, `end_time` and optional `environment`. This non-cacheable
read returns the structured report directly. Unknown/repeated query parameters are refused.
REST/dashboard require `tracking_plans:read` and `analytics:read`; MCP additionally requires
`mcp:read`. The shared backend rechecks the retained declaration and current authority before
returning, including current content permission when originally granted.

Compare one exact declared event with permitted stored observations in the target project:

```bash
anectico --project "$PROJECT_ID" analytics tracking-plans drift "$PLAN_ID" --revision 2 --event checkout_completed --start 2026-09-13T00:00:00Z --end 2026-09-14T00:00:00Z --environment production
```

Requires `tracking_plans:read`, `analytics:read` and `mcp:read`. Additional content access and
current policy govern which stored property keys and counts may be disclosed. Use MCP
`get_tracking_plan_drift` through `execute_read_action` with `project_id`, `plan_id`, a positive
`revision` string, `event`, `start_time`, `end_time` and optional `environment`. Empty environment
selects all environments. The half-open event-time window accepts millisecond precision and spans
at most 90 days, subject to current readable retention and query limits.

The result includes exact plan/revision/hash coordinates and `report_json`, containing escaped
JSON inside the usual single untrusted wrapper. Decode it as data, never instructions. The report
includes the full permitted schema observation, selection, first-receipt cutoff, observation times,
unknown platform/upstream completeness, and a comparison per declared or disclosed property.
Counts are bounded by the schema reader's 250,000-event limit; exceeding a query budget refuses the
report rather than truncating it into an apparently complete result.

- `warning`: a disclosed property has an observed undeclared JSON type, or a required property is
  missing from some stored events in the selected sample. Required means present: null is a value,
  and is permitted only if `null` is among the declared types.
- `observed_match` on a property, or `no_observed_mismatch` overall: no mismatch was observed in the
  disclosed sample. Neither means healthy deployment, complete delivery or plan-wide conformance.
- `unknown`: zero selected events or no disclosed schema row for a declared property. The key may
  have been withheld or never observed. Missing/present counts remain null; the report does not
  convert unavailable evidence into a missing-field violation.
- `undeclared`: a disclosed stored property has no declaration in this event. This is informational;
  tracking plans are not closed capture allowlists.

A warning takes precedence in the overall status, but unknown property rows remain visible. The
report compares the selected retained revision even if a newer head exists. Retired plans are
unavailable. `ingested_before` is a server first-receipt cutoff, not a processed watermark or frozen
snapshot; repeated reads can change as data arrives. Each event check is independent. This action
never changes a plan or rejects a capture, and it cannot prove a complete critical journey.
