Manage tracking plans
Ask your agent to validate, preview and manage versioned expected instrumentation through MCP or the CLI, and to compare a plan with what was captured.
On this page
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 to inspect permitted stored observations separately.
Your agent lists, reads, creates, replaces and retires plans through MCP or the CLI. Every write previews and validates the proposed configuration before confirmation. Portable document validation, export, import and change review are also available. Pinned event drift and optional browser and mobile journey doctor comparisons compare a plan with what was captured. The Console has no tracking-plan pages. Tracking plans do not reject otherwise valid captured events; existing ingest safety checks still apply.
Ask your agent
"Create a tracking plan called 'Checkout' that declares
checkout_completedwith a required numericamountand a sensitive stringcontact. Show me the preview first."
"Compare revision 2 of the 'Checkout' plan with what we captured for
checkout_completedbetween 13 and 14 September, in production. Tell me which properties have a warning."
| Job | MCP tool or action | CLI command |
|---|---|---|
| List plans | list_tracking_plans |
anectico analytics tracking-plans list |
| Read the current or an exact retained revision | get_tracking_plan |
anectico analytics tracking-plans get |
| Create a plan | create_tracking_plan |
anectico analytics tracking-plans create |
| Replace a plan | update_tracking_plan |
anectico analytics tracking-plans update |
| Retire a plan | delete_tracking_plan |
anectico analytics tracking-plans delete |
| Check a portable file | validate_tracking_plan |
anectico analytics tracking-plans validate |
| Export an exact revision as a portable file | get_tracking_plan with format: "portable" |
anectico analytics tracking-plans export |
| Import a portable file as a new plan | create_tracking_plan with document |
anectico analytics tracking-plans import |
| Review and apply a change to an exact revision | update_tracking_plan with document |
anectico analytics tracking-plans diff and apply |
| Compare one declared event with stored observations | get_tracking_plan_drift |
anectico analytics tracking-plans drift |
| List, read and apply starter recipes | list_instrumentation_recipes, get_instrumentation_recipe, apply_instrumentation_recipes |
anectico recipes list, show and apply |
What your agent gets back
A read returns the plan with its full normalized definition, a definition_hash, the exact
decimal-string revision and the time. A write preview returns the current and the proposed
configuration in escaped untrusted JSON. Both text and structured previews mark authored values;
preview.details.before and proposed carry the complete definitions and required_scopes
names the permissions. A confirmed write returns a receipt with IDs, revisions and
the definition hash. A drift read returns a structured report in report_json. See
Observe drift for a pinned event.
Open the proof
A tracking plan has no proof page, and no tool in this guide returns a link. A plan is configuration. It does not prove that an SDK is deployed or that events were captured. To show what was actually captured, ask your agent to read the event property schema and report its numbers, or to run a measurement whose result opens as a read-only page; see Measure product events.
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 and delete scopes remain outside the OAuth ceiling.
See tracking-plan errors for project-selection refusals, configuration failures and recovery after an uncertain save.
Writes additionally require tracking_plans:write for create or 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.
A save whose acknowledgement was lost is recovered by repeating the identical request with the same mutation UUID; see Create, replace and retire.
CLI
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 and 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:
{"action":"list_tracking_plans","arguments":{"project_id":"PROJECT_UUID","limit":50}}
{"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 and 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 and revision.
No raw captured event values, actor and 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.
Start from a recipe
Recipes are a small server-owned catalog of common business events — signup, activation, invitation and purchase — with SDK snippets and starter insights, so a new project can record a first business journey without hand-authoring a definition. See Record your first business journey for the walkthrough.
Purchase's Revenue by plan starter reads the purchased event's amount and currency
properties. It shows the last seven days' revenue broken down by plan in a separate section for
each currency, with no combined total or currency conversion. See Measure revenue.
The Paying accounts starter counts distinct companies that purchased each day.
Starter insights measure the last seven days, the minimum supported product history window, so they can run on a project with seven days of retention. You can edit a saved insight to use a longer window when your readable history supports it. Existing saved revisions keep their original windows; for an older starter insight with a 30-day window, edit it to the last seven days, save a new revision, then run that revision.
Applying a selection of recipes writes to exactly one tracking plan per project, named "Recipes"
(created on first use). A recipe's events replace any existing event of the same name already in
that plan; every other event already in it is kept. The same call also creates or updates one saved
insight per recipe's starter insight (see saved insights).
Re-applying an identical selection from the same catalog version, with nothing else changed since,
writes nothing: both the plan and every insight report applied: false. Applying a new catalog
version creates new starter insights and leaves older saved insights and revisions unchanged.
For a Purchase starter from catalog v1, v2 or v3 that sums amounts without separating currencies,
apply the current Purchase recipe and use the newly created Revenue by plan insight.
If you have deleted the "Recipes" plan, a later apply skips it. Deleted starter insights are skipped
when you apply the same catalog version again. Skipped targets are listed in the response's
skipped field with their reason, and every other target in the selection still applies.
You can keep editing the "Recipes" plan and its insights afterward like any other tracking plan or saved insight. Applying a recipe again later still only replaces the events that recipe declares, so a customization you made to an event you added yourself, or to an unrelated event, survives.
anectico recipes list
anectico recipes show signup
RECIPE_RETRY_KEY="$(uuidgen)"
anectico recipes apply signup activation --catalog-version v5 --idempotency-key "$RECIPE_RETRY_KEY"
anectico recipes apply signup activation --catalog-version v5 --idempotency-key "$RECIPE_RETRY_KEY" --confirm-token "$TOKEN"
Without --confirm-token, apply previews the exact plan change and insights that would be
written, with a diff summary, and writes nothing; repeat the same arguments and retry key with the returned token
to apply. Applying requires tracking_plans:read, tracking_plans:write, insights:read and
insights:write, plus mcp:read and mcp:write for an API key.
MCP discovers the catalog with list_instrumentation_recipes and get_instrumentation_recipe
through execute_read_action, and applies a selection with the confirmed
apply_instrumentation_recipes action: the first call previews (writes nothing), and repeating the
identical arguments with confirm_token applies it.
Preview, applied and replayed structured receipts all use operation: update.
The preview contains the complete before and proposed configuration. Applied and replayed
receipts identify the committed targets for read-back.
REST: GET /api/v1/analytics/recipes, GET /api/v1/analytics/recipes/{id}, and
POST /api/v1/analytics/recipes/apply with a body of project_id, recipe_ids, catalog_version
and a stable UUID idempotency_key. The route supports a current signed-in user session
or an appropriately scoped API key. The first call previews; after approval, repeat the
same body and key with X-Anectico-Confirm-Token from the preview. catalog_version
pins the catalog version the request was authored against; omit it to accept the server's current
version.
Create, replace and retire
Save a full version 1 definition in checkout.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:
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 or update) name and definition. Send expected_revision as an
exact decimal string for update or delete, and add confirm_token on the second identical call.
Discover these actions through list_write_actions. Previews contain current and 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 and 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.
Recover an uncertain save
If the acknowledgement of a write is lost, or a later refusal hides the result, you cannot tell whether the write committed. Your agent keeps the exact original request and its mutation key, and repeats that same request. It does not make a fresh key: a fresh key can create another revision. If the repeat is refused, a not-found answer or a comparison with the current plan still cannot show that the earlier write failed. Restore access to the original project, then repeat the same request again. The receipt of a repeat describes the original operation, even if another change has since moved the live plan.
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 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 or 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:
{"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.
# 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:
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 and 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 or 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
Drift compares one declared event of an exact plan revision with the permitted stored observations in an explicit window. Your agent names the plan, a positive revision, the event, an explicit UTC start and exclusive end, and an optional exact environment. Each comparison is a fresh observation, so its first-receipt cutoff and counts may differ from an earlier journey summary.
Each property row carries the declared types and required flag next to the disclosed present and
missing counts, the observed type counts and the unexpected types. Evidence that is unavailable
keeps null counts. It never becomes an invented missing-field violation. A property the plan does
not declare is reported as undeclared, which is informational.
SDK metadata $release and $app_version is treated as optional string properties when observed,
even if your plan does not declare them. Their rows carry reserved: true in the report. An
explicit declaration keeps your chosen types and required setting. Declared metadata with no
disclosed observation stays unavailable, with null counts; unobserved metadata
that you did not declare adds no row. Application-property warnings remain visible alongside it.
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 or repeated query parameters are refused.
REST requires 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:
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 undeclared 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, and 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 and 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.
Every property the pinned revision declares for the event is measured, custom names included
(plan, amount, activation_step): the report counts its JSON types and how often it is present
or missing, never its values, because the name comes from your own plan rather than from discovery.
That is what lets drift flag a declared property sent as a string in some events and a number in
others. A declared name that is itself an AI content field (a prompt, completion or tool-argument
attribute) still needs content access and a policy that permits it. Properties the plan does not
declare stay under the ordinary content rules, so a custom undeclared key may not appear at all.
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 ifnullis among the declared types.observed_matchon a property, orno_observed_mismatchoverall: 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 was never observed in the selection, or it is an AI content field your access or policy withholds. Missing or 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.
Did you mean
A drift report carries two "did you mean" annotations. The comparison itself is unchanged by both: a suggestion never edits a plan, never changes a property outcome or the overall status, and never hides or reorders a row.
Undeclared events lists the event names observed in the same window that the pinned revision does not declare, each with its observed count when one is available. The list is separate from the property comparison, which stays scoped to the one event you selected. A read stops at a bound, and says so when more undeclared names may exist.
Per-key suggestions annotate the undeclared rows in the property table — the stored property
keys this event declares nothing about.
In both places the annotation is the declared name the observed one was most likely meant to be
(suggested_name), shown only when the judgment was confident enough to stand behind. Otherwise
unavailable_reason says why there is none:
pending— the name has been recorded but not judged yet. Read the report again later and the answer will be there. The first report for a newly observed name is normally pending, because names are judged away from your request rather than while you wait for it.none— the name was judged and is a genuinely new one, not a variant of anything declared. Add it to the plan or leave it undeclared; both are ordinary.below_bar, or no reason at all — the judgment was not confident enough to show. A weak guess is not shown as a weaker one.key_not_benign— the property key is a custom one, so it is only considered for someone who already has content access to this project. The key itself is never sent for a suggestion otherwise, and its values are never sent in any case.disabledorcredential— a configuration state rather than a pause: suggestions are turned off for your organization, or the suggestion service refused your deployment's access. Neither clears until the setup is changed.- Any other reason — the judgment could not be made this time, usually because the daily decision-scoring cap is reached (see plans and limits) or the model was briefly unavailable. Names stay recorded and are judged on a later pass.
Whole-report unavailability is stated rather than hidden: if the annotations could not be read at all, the report says so, and the property comparison is still complete and correct.
REST returns undeclared_events (an array, or null when the annotations could not be read at
all), undeclared_events_truncated, an optional suggestions_unavailable_reason, and a
suggestion object on each undeclared property row. MCP and CLI carry the same fields inside
report_json. An empty undeclared_events array means no undeclared event name was observed;
null means nothing about undeclared names was readable, which is a different fact.
Recipe application shows complete current and proposed event and starter-insight definitions.
Keep one UUID idempotency_key (--idempotency-key in the CLI) across preview, confirmation
and retries. The whole selection commits together at the previewed state. A retry returns the
original result even after later edits. A stale preview requires a fresh preview.
Tracking-plan sensitivity does not disable capture redaction. In the signup recipe,
recognized email property values become [REDACTED] during capture, whether or not
the plan marks them sensitive. Join people with a stable id. See
Redaction for recognized shapes and limits.