---
name: xnurta-query-operation-log
description: >-
  Query Xnurta platform operation logs: user and AI action history on ad entities.
  For tracking change history, auditing operations, troubleshooting issues.
  Keywords: operation log, change history, who changed what, bid adjustment records,
  budget adjustment, AI auto-adjustment, pause/enable records, operation audit,
  modification timeline, ad adjustment log
metadata:
  version: 1.2.1
---

# Query Operation Log Skill

## MCP Tool

This skill maps to MCP tool: **`get_operation_log`**. Required scope: `amazon_sa_ads_logs:read`.

Profile, tenant, and user scope are resolved from the authenticated bearer token. `profileIds` is **required** — always call `get_user_authorized_context` first to obtain authorized profile IDs, then pass one or more into `profileIds`. **Every requested ID must be authorized: one unauthorized value fails the entire call** with `invalid_params` / `Requested profileIds contain unauthorized values` — nothing is silently dropped (see Platform-Wide Rules below). If the user didn't specify a store, pass **all** authorized `profileIds`. Never pass `tenantId` or `userId`; the server derives them from the token.

**⚠️ On this tool, how many `profileIds` you pass also changes the timezone of the timestamps you get back.** Read "createdDate Timezone" below before you report any time to the user.

**`userContext` (required)**: Must pass a non-empty string on every call. Preserve the user's original query as much as possible, plus the agent's reason for calling this tool. Summarize if too long, max 100 characters.

## Platform-Wide Rules

**Before using this tool, read [`references/platform-notes.md`](references/platform-notes.md)** — it covers auth flow, permission scopes, error handling, pagination/date-limit tables, currency rules, the tool-selection decision tree, and implicit inference rules shared across all 3 read tools. That file ships inside this same skill folder, so it travels with this skill regardless of how it's packaged/installed. This SKILL.md only covers what's specific to `get_operation_log`.

## ⚠️ Two Pagination Modes (decided by `entities`)

`get_operation_log` behaves in one of two modes depending on `entities`. This is the most important thing to get right.

**Mode A — Real pagination** (when `entities` is explicitly set to **exactly one non-`aiGroup` entity**, e.g. `["campaign"]`):
- Supports `page` (1-based, default 1) + `pageSize` (default 100, max 1,000). Response includes `page`, `pageSize`, `hasNextPage`.
- **To get the complete history, loop `page` (incrementing each call) while `hasNextPage=true`.** This is the preferred way to retrieve a complete change history for one entity type — no date-splitting needed. (`hasNextPage` is the real-pagination equivalent of `truncated`.)

**Mode B — limit-only** (when `entities` is empty/omitted, has **multiple** entities, or is **only `aiGroup`**):
- `page` is ignored. A single call returns at most `pageSize` rows (default 100, max 1,000; **`aiGroup`-only max 10,000**), time-descending. Response includes `limit` and `truncated`.
- `truncated=true` means more matched than could be returned. **You cannot page.** Handle it in this order:
  1. **Prefer steering the user to a single entity.** If they really only care about one kind of change ("just budget changes", "just campaign pause/enable"), guide them to narrow `entities` to a single non-`aiGroup` entity → this switches to Mode A, where you can page through the complete set. Only do this when it matches the user's actual intent; if they genuinely want the full cross-entity history, don't silently narrow — say the result is a partial view.
  2. **Split the date range** into non-overlapping sub-windows and recurse (see "Getting a Complete Count" below).
  3. **Add filters** (`resourceIds`/`actionType`/`operationType`/`changeBy`) as a last resort — this changes *what* you're searching for, so flag the result as partial when you do.
  4. `aiGroup` is inherently limit-only (but its 10,000 cap is usually enough for a window).

## When to Use

Use this tool when the user needs any of the following:
- Query ad operation history
- Find out who made what changes (manual vs AI)
- Track AI auto-adjustment records (bid, budget, etc)
- View bid/budget adjustment history
- Audit a specific campaign's change timeline
- Troubleshoot ad status changes (pause, enable, archive)
- Count how many changes of a given type happened, grouped by operator or entity (do this by pulling rows and aggregating client-side — the tool itself has no server-side `groupBy`)

See the Tool Selection Decision Tree above if the user's ask might belong to `get_ads_perf` or `get_entity_metadata` instead.

## DSL Parameter Format

```json
{
  "userContext": "User's original query + agent's reason for calling",
  "dateStart": "YYYY-MM-DD",
  "dateEnd": "YYYY-MM-DD",
  "profileIds": [1234567890123456],
  "entities": ["entity_type_list"],
  "resourceIds": [{"idEntity": "campaign", "ids": [123456]}],
  "changeBy": {"operator": "IN", "values": ["ai", "manual"]},
  "actionType": {"operator": "IN", "values": ["Bid Increased"]},
  "operationType": {"operator": "IN", "values": ["Campaign Paused"]},
  "campaignTypes": ["campaign_type_list"],
  "targetTypes": ["target_type_list"],
  "placementTypes": ["placement_type_list"],
  "page": 1,
  "pageSize": 100
}
```

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| profileIds | array[long] | **Yes** | — | Profile IDs from `get_user_authorized_context.profileIds`. Intersected with token's authorized set |
| dateStart | string | **Yes** | — | Start date `YYYY-MM-DD`. Max span vs `dateEnd` is 90 inclusive calendar days (see platform-notes.md for the precise off-by-one-safe definition); cannot be more than 15 months before today |
| dateEnd | string | **Yes** | — | End date `YYYY-MM-DD` |
| userContext | string | **Yes** | — | User's original query + reason, max 100 chars |
| entities | array[string] | No | all entities except `audience` | Entity type filter |
| resourceIds | array[object] | No | — | Resource ID filter (see below) |
| changeBy | object | No | — | Operator filter (see below) |
| actionType | object | No | — | Action type filter — coarse-grained (see below). ⚠️ **Only use values from the [actionType enum table](references/field-reference.md#actiontype)** — do not invent or guess values |
| operationType | object | No | — | Operation type filter — fine-grained (see below). ⚠️ **Only use values from the [complete operationType table](references/field-reference.md#complete-operationtype-values-by-entity)** — do not invent or guess values |

> **Recommended strategy**: when unsure which exact `operationType` values to use, first query with `actionType` (coarse-grained) to explore results and identify the specific `operationType` values appearing in the returned rows, then follow up with `operationType` (fine-grained) for precise filtering.
| campaignTypes | array[string] | No | — | Campaign type filter |
| targetTypes | array[string] | No | — | Targeting type filter (see below) |
| placementTypes | array[string] | No | — | Placement type filter (see below) |
| page | int | No | 1 | Page number (1-based). **Only effective when `entities` is exactly one non-`aiGroup` entity** (Mode A); ignored for multi-entity or `aiGroup`-only queries (Mode B) |
| pageSize | int | No | 100 | Max rows per page (Mode A) / per call (Mode B). Non-`aiGroup` max **1,000**; **`aiGroup`-only max 10,000**. Results are time-descending |

## Parameter Details, ChangeLogVO Field Reference

**See [`references/field-reference.md`](references/field-reference.md)** for the complete parameter enum reference (entities, resourceIds, campaignTypes, changeBy, actionType, operationType, targetTypes, placementTypes) and the ChangeLogVO response field table.

## Response Structure

```json
{
  "isError": false,
  "toolName": "get_operation_log",
  "rows": [
    {
      "entity": "campaign",
      "entityName": "Brand-SP-Auto-US",
      "amazonEntityId": 298539385213868,
      "entityId": 298539385213868,
      "operationType": "DailyBudget Increased",
      "profileId": 4404871489220462,
      "aiGroupName": "Growth-Group-1",
      "amazonCampaignId": 298539385213868,
      "campaignName": "Brand-SP-Auto-US",
      "campaignType": "sponsoredProducts",
      "amazonAdGroupId": null,
      "adGroupName": null,
      "changeField": "dailyBudget",
      "previousValue": "50.0",
      "newValue": "65.0",
      "countryCode": "US",
      "currencyCode": "USD",
      "createdDate": "2024-06-15 14:30:00",
      "changedBy": "ai"
    }
  ],
  "rowCount": 1,
  "limit": 100,
  "truncated": false,
  "effectiveProfileIds": [4404871489220462]
}
```

The pagination fields differ by mode: **Mode A (real pagination)** returns `page`, `pageSize`, `hasNextPage`; **Mode B (limit-only)** returns `limit`, `truncated` (as shown above).

## ⚠️ `createdDate` Timezone (depends on entity + profileIds count)

**`createdDate` is not always UTC. Its timezone depends on which entity the row belongs to and on how many `profileIds` you passed.** Confirmed by testing (2026-08):

| Entity | profileIds count | `createdDate` timezone | Example |
|---|---|---|---|
| `aiGroup` | one or many | **UTC** | `2026-08-24 05:58:50` |
| `campaign` / `adGroup` / `target` / `placement` | **one** | **Store-local** | `2026-08-24 00:01:06` (LA time) |
| `campaign` / `adGroup` / `target` / `placement` | **many** | **UTC** | `2026-08-24 07:01:06` (UTC) |

Verified on one pause record for a US profile (`America/Los_Angeles`, UTC-7 in August):
- `profileIds: [4404871489220462]` → `"2026-08-24 00:01:06"`
- `profileIds: [4404871489220462, 2618208845223116]` → `"2026-08-24 07:01:06"`

Same event, 7 hours apart — exactly the LA daylight-saving offset.

### What you must do about it

1. **Always label the timezone when you show a timestamp.** "2026-08-24 07:01 UTC" or "2026-08-24 00:01 store time". An unlabeled timestamp from this tool is ambiguous by construction, and the ambiguity is a whole business day near midnight.
2. **Never merge or compare a single-profile result with a multi-profile result.** They're on different clocks. If you already pulled one store and then widen the scope, **re-pull** — don't stitch.
3. **Watch for mixed clocks inside one response.** `entities: ["campaign", "aiGroup"]` on a single profile returns store-local campaign rows *and* UTC aiGroup rows in the same list. Rows are sorted on the raw timestamp string, so a mixed-clock result is **not reliably chronological** — don't present it as an ordered timeline, and don't compute "X happened before Y" across entity types.
4. **Pick the mode deliberately:**
   - The user wants times they can match against their Amazon console → query **one profile at a time**, restrict `entities` to ad entities (`campaign`/`adGroup`/`target`/`placement`), and label the output store-local.
   - The user wants a comparable cross-store timeline → query **multiple profiles** (everything comes back UTC) or convert client-side using each row's `countryCode`, and label the output UTC.
5. **Don't equate `createdDate` with the `dateStart`/`dateEnd` boundary.** Which zone the request-level filter uses is not documented, so a row can sit near the edge of your requested window in one clock and outside it in another. For boundary-sensitive questions ("what changed yesterday" on a non-UTC store), say results at the edges are approximate.
6. **Don't derive "AI acts at night" style conclusions from raw timestamps** without first pinning down which clock you're in — an hour-of-day pattern computed on UTC rows for a US store is shifted by 7-8 hours.

The MCP layer does no conversion; this is how the log data itself is returned.

| Field | Type | Description |
|---|---|---|
| `isError` | boolean | Whether the call errored — check this before reading `rows` |
| `toolName` | string | Tool name |
| `rows` | array[ChangeLogVO] | Log entries, time-descending |
| `rowCount` | int | Number of rows returned on this page/call |
| `page` / `pageSize` | int | **Mode A only.** Current page and page size |
| `hasNextPage` | boolean | **Mode A only.** `true` → fetch `page+1` to continue; loop until `false` for the complete set. (Semantically equivalent to Mode B's `truncated`) |
| `limit` | int | **Mode B only.** The `pageSize` cap actually applied |
| `truncated` | boolean | **Mode B only. `true` means more matched than `limit` could return.** You cannot page — steer the user to a single entity (→ Mode A), else split the date range into non-overlapping sub-windows, else add filters as a last resort (flagging the result as partial) |
| `hint` | string | Guidance message, present when `truncated=true` |
| `effectiveProfileIds` | array[long] | Profile IDs the query ran against — an echo of your request (unauthorized IDs fail the call outright) |
| `requestId` | string | Trace ID — quote it when reporting a failure to the user. May be absent locally |
| `currency` | string | Optional roll-up of the rows' currencies: a single code if all rows agree, the literal **`"mixed"`** if they don't, absent if no row carries one. **`"mixed"` is not a currency** — fall back to each row's `currencyCode`, and never sum or format amounts across a mixed result |

On error, the response instead follows the shared error envelope described in Platform-Wide Rules above (all errors use a single top-level `errorType`, tool and pipeline alike, e.g. `rate_limited`).

## Getting a Complete Count (handling large result sets rigorously)

There is no server-side `groupBy` for this tool — to answer "how many budget changes did each operator make" or "count of placement adjustments per campaign" **completely and correctly**, first pull the complete row set, then aggregate client-side. How you get the complete set depends on the mode:

**If you can scope to a single non-`aiGroup` entity (Mode A) — preferred:** loop `page` (incrementing each call) while `hasNextPage=true`, collecting rows; raise `pageSize` toward the max (1,000) to cut round trips. Once `hasNextPage=false` you have the complete set — aggregate and you're done. No date-splitting needed.

**Otherwise (Mode B: multi-entity or `aiGroup`-only) — use date-splitting:**
1. Issue the query with your filters and check `truncated`.
2. **If `truncated=false`**: the rows are complete for that window — aggregate client-side and you're done.
3. **If `truncated=true`**: split the date range into two **non-overlapping** sub-windows (bisect at the midpoint, second half starting the day after the first half ends — never share a date), and recursively repeat on each half.
4. Only sum/merge counts across **non-overlapping** sub-windows — overlapping ranges double-count.
5. **If a single day still returns `truncated=true`** on its own (more than the cap — 1,000, or 10,000 for `aiGroup`-only — in one day for your filters), this tool **cannot guarantee a complete count** for that day. Don't report a partial number as exact — tell the user it's a lower bound (e.g. "at least 1,000 changes on 2024-06-15; can't return an exact count for a single day this active") and suggest scoping to a single entity (Mode A) or narrowing by `resourceIds`/`operationType`.

## Examples

**Query last 7 days of AI auto-adjustment records:**
```json
{
  "profileIds": [4404871489220462],
  "dateStart": "2026-07-13",
  "dateEnd": "2026-07-19",
  "changeBy": {"operator": "IN", "values": ["ai"]},
  "pageSize": 100,
  "userContext": "Last 7 days of AI auto-adjustments"
}
```

**All operations on a specific campaign (resolve name to ID first via get_entity_metadata, then):**
```json
{
  "profileIds": [4404871489220462],
  "dateStart": "2026-06-01",
  "dateEnd": "2026-06-30",
  "resourceIds": [{"idEntity": "campaign", "ids": [298539385213868]}],
  "pageSize": 1000,
  "userContext": "Full change history for this campaign"
}
```

**Budget increases made manually by users (not AI/automation):**
```json
{
  "profileIds": [4404871489220462],
  "dateStart": "2026-06-01",
  "dateEnd": "2026-06-30",
  "actionType": {"operator": "IN", "values": ["Budget Increased"]},
  "changeBy": {"operator": "IN", "values": ["manual"]},
  "pageSize": 100,
  "userContext": "Manual budget increases this month"
}
```

## Notes

- **`operationType` and `actionType` are closed enums** — you MUST only use values listed in [`references/field-reference.md`](references/field-reference.md). Never invent, guess, or interpolate values. If unsure whether a value exists, consult the reference table before sending the request. Values are English display strings (e.g. `"DailyBudget Increased"`, `"Campaign Paused"`) — pass them exactly as listed, case-sensitive
- `dateStart`/`dateEnd` request params use `YYYY-MM-DD`; `createdDate` in returned rows is a full timestamp — **but its timezone varies** (`aiGroup` = UTC; ad entities = store-local for one `profileId`, UTC for several). Always label the zone, never merge single- and multi-profile results, and treat a mixed-entity result as not reliably chronological. See "createdDate Timezone" above
- `dateEnd` must be equal to or later than `dateStart`
- Max date span is 90 days, max lookback is 15 months — these are hard limits, not auto-truncation. Split longer windows into multiple calls
- **Pagination has two modes (decided by `entities`)**: a single non-`aiGroup` entity → real pagination (`page` + `pageSize`, loop until `hasNextPage=false`); multi-entity or `aiGroup`-only → limit-only (`pageSize` caps the call, max 1,000 / `aiGroup` 10,000, check `truncated`). For a complete count in limit-only mode, prefer steering to a single entity, otherwise use the non-overlapping date-split procedure above
- When `entities` is not specified, returns operations on all entity types **except `audience`**
- `profileIds` is **required**. Always call `get_user_authorized_context` first. If the user doesn't name a store, pass all authorized `profileIds` — but note this also switches ad-entity timestamps to UTC (see above)
- **Every requested `profileId` must be authorized** — one bad value fails the whole call (`Requested profileIds contain unauthorized values`); nothing is silently dropped
- `pageSize` on this tool is **clamped** silently (over the max → capped, no error). This differs from `get_ads_perf`/`get_entity_metadata`, where an out-of-range `pageSize` is an error
- `changeBy`, `actionType`, and `operationType` must be passed as objects with explicit `operator`/`values` (not bare arrays). **Incomplete filters are rejected with `invalid_params`** (no longer silently ignored): a missing `operator`, or `values` that is `null` / an empty array / all-`null`, all fail — build the full `{"operator": "IN", "values": [...]}` object or omit the filter entirely.
- **`resourceIds` is strictly validated too**: each item must be an object with `idEntity` and an `ids` array of numbers — a missing `idEntity`, a non-numeric ID, or a non-array `ids` returns `invalid_params` (previously such items were silently dropped, e.g. "3 IDs requested, only 2 queried"). **Exception:** an explicit `ids: null` is a *reserved* value meaning "no ID restriction" (different from `[]`, which means the scope genuinely has no results) — it is accepted, not rejected.
- To find a resource by name (campaign name, keyword text, ASIN) rather than ID, resolve it first via `get_entity_metadata`, then pass the ID into `resourceIds`
- No server-side aggregation (`groupBy`) — aggregate client-side after pulling rows, splitting non-overlapping date windows whenever `truncated=true`
- When `profileIds` spans multiple stores, map each row's `profileId` to a `profileName` before describing results
- Amounts in `previousValue`/`newValue` are always local currency — read the row's `currencyCode`, don't assume USD. The optional top-level `currency` may be the literal `"mixed"` on cross-store results; that's a marker, not a currency
- `placementTypes` has exactly 4 confirmed values — don't invent additional ones
- On error, check `errorType` and handle per the guidance above (e.g. `rate_limited` means wait `retryAfterSeconds` before retrying, not that the question is unanswerable)

## Reference Docs

- Shared cross-tool behavior (auth, errors, pagination, dates, currency, decision tree, inference rules): [`references/platform-notes.md`](references/platform-notes.md)
- Field dictionary (parameter enums, ChangeLogVO fields): [`references/field-reference.md`](references/field-reference.md)
- Enum i18n (ZH/EN/JA display labels for changeBy, actionType, operationType, entity — use this when presenting log entries to the user or translating between API values and localized display text): [`references/enum-i18n.md`](references/enum-i18n.md)
- Query examples:
  - [AI bid adjustment records (and how to broaden to all AI ops)](references/example-ai-bid-changes.md)
  - [All bid-change operations on a specific campaign](references/example-campaign-operations.md)
  - [SP budget changes (fine-grained filter)](references/example-user-budget-changes.md)
  - [Pause operations](references/example-pause-operations.md)
  - [Resolving a name to an ID before querying logs](references/example-resolve-name-to-id.md)
