v1.2.0 to v1.2.1

4 added, 3 removed. Audit A to A.

---
name: sparkx-query-operation-log
description: >-
Query SparkX AI 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.0
+ 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 (two shapes: `errorType` for tool errors, `error` for pipeline errors such as `rate_limited`).
+ 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` should be passed as objects with explicit `operator`/`values` (not bare arrays)
+ - `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)