0.0 / 30
What changed in the harness
Selection accuracy 98→100, token cost up 8%, unconfirmed writes 0%→0%.
Category breakdown
Where the score comes from.
Earned points across the four signals Gradable measures. Safety and Legibility are scored out of 30; Economics and Discoverability are scored out of 20.
01Safety
02Legibility
18.5 / 30
03Economics
7.3 / 20
04Discoverability
11.7 / 20
Highest-impact fix
Estimated gain +30 pointsAdd explicit identity and permission preflight tools
Expose machine-readable principal/tenant confirmation and a non-mutating permission check so agents can verify both before destructive actions.
Description evidence
Defects and rewrites.
67 defects found across the exposed tool descriptions. Suggested rewrites make purpose, inputs, boundaries, and returns easier for an agent to understand.
| Tool | Defect types | Suggested rewrite |
|---|---|---|
search_geo |
params_unexplained no_return_description |
Search geo entities (Country / AdminArea / Locality) by `query`, the name (or partial name) of the geo you are looking for when building geo targeting. Returns the matching geo entities; pass `countrycode` (lowercase, on purpose, as Apple is case-sensitive here) to scope an AdminArea or Locality search to one country, and use `limit`/`offset` to paginate. |
apps_get |
params_unexplained |
Fetch App Store metadata for the app with the given numeric `adamId` (the App Store app id, found via `search_apps`). Returns appName, developerName, primaryGenre, secondaryGenre, iconPictureUrl, availableStorefronts, deviceClasses, and other metadata. |
apps_locale_details |
params_unexplained |
Get the localized default product-page details for the app with the given numeric `adamId` (the App Store app id, found via `search_apps`). Returns, per locale, the subtitle, short description, screenshots, and app preview used on the default product page. |
apps_eligibilities_find |
params_unexplained |
Find app-eligibility records for the app with the given numeric `adamId` (the App Store app id, found via `search_apps`), using `selector` conditions (filterable by supplySource, countryOrRegion, deviceClass, state) plus orderBy/pagination. Returns the matching eligibility records. |
apps_assets_find |
params_unexplained |
Find App Store screenshot / app-preview assets for the app with the given numeric `adamId` (the App Store app id, found via `search_apps`), using the `selector` to filter. Returns assetGenIds you can use to author creatives or audit rejected assets. |
cpp_list |
params_unexplained |
List the Custom Product Pages configured for the app with the given numeric `adamId` (the App Store app id, found via `search_apps`). Returns the app's Custom Product Pages with their ids, usable with `cpp_get` and `cpp_locale_details`. |
cpp_get |
params_unexplained |
Fetch a single Custom Product Page for the app with the given numeric `adamId` (the App Store app id), identified by its `productPageId` (obtained from `cpp_list`). Returns that Custom Product Page's full details. |
cpp_locale_details |
params_unexplained |
List the locales available for a Custom Product Page of the app with the given numeric `adamId`, identified by its `productPageId` (obtained from `cpp_list`). Returns the available locales, each with its localized assets. |
campaigns_create |
params_unexplained no_return_description |
Create a new campaign and return the created campaign including its id. The minimum useful body specifies adamId, name, countriesOrRegions, adChannelType, supplySources, billingEvent, and a budget. Set `paymentModel` (LOC, PAYG, or NOTCARD) to match your account's billing type; LOC accounts additionally use `locInvoiceDetails` and `budgetOrders`. |
campaigns_get |
params_unexplained |
Fetch one campaign identified by its numeric `campaignId` (obtained from `campaigns_list` or `campaigns_find`). Returns the campaign's full details, including name, status, budgets, schedule, and targeting settings. |
campaigns_find |
no_return_description |
Search for campaigns matching the `selector` conditions (operators such as EQUALS, IN, CONTAINS; supports fields, orderBy, and pagination). Returns the matching campaigns with the requested fields, e.g. to locate a campaignId for `campaigns_get` or `campaigns_update`. |
campaigns_update |
params_unexplained no_return_description |
Update the campaign identified by the numeric `campaignId` (obtained from `campaigns_list`), passing a partial `campaign` object with only the fields you want to change. Set clearGeoTargetingOnCountryOrRegionChange=true if you change countriesOrRegions so ad-group geo targeting is cleared. Returns the updated campaign. |
campaigns_delete |
name_restates_behavior params_unexplained no_return_description |
Remove the campaign identified by the numeric `campaignId` (obtained from `campaigns_list`) from your org. Deleting is permanent and cannot be undone, so confirm the id first. Returns a success response once the campaign is deleted. |
adgroups_create |
params_unexplained no_return_description |
Create a new ad group within the campaign identified by `campaignId` (obtained from `campaigns_list`/`campaigns_find`) and return the created ad group. `name` is required (max 200 chars); `endTime` is an optional ISO 8601 end datetime; `status` may be ENABLED or PAUSED (default ENABLED); use `extra` to pass additional targeting fields verbatim to Apple. Bid and targeting live on the ad group, not the campaign. |
adgroups_get |
params_unexplained |
Fetch one ad group identified by its numeric `adGroupId`, scoped to the campaign with the given `campaignId` (both obtainable from `campaigns_list` and `adgroups_list`). Returns the ad group's details, including its bids, status, and targeting dimensions. |
adgroups_list |
params_unexplained |
List all ad groups belonging to the campaign with the given `campaignId` (obtained from `campaigns_list` or `campaigns_find`), paginated via `limit`/`offset`. Returns the campaign's ad groups. |
adgroups_find_in_campaign |
params_unexplained no_return_description |
Find ad groups within the campaign identified by `campaignId` (obtained from `campaigns_list` or `campaigns_find`) that match the `selector` conditions (operators like EQUALS/IN/CONTAINS, with orderBy and pagination). Returns the matching ad groups with the requested fields. |
adgroups_find_org_wide |
no_return_description |
Find ad groups across the entire org that match the `selector` conditions (operators like EQUALS/IN/CONTAINS, with orderBy and pagination). Useful when you do not know the parent campaignId. Returns the matching ad groups with the requested fields. |
adgroups_update |
params_unexplained no_return_description |
Update an existing ad group identified by campaignId (the campaign it belongs to) and adGroupId. The adGroup object carries the fields to change — set only the ones you want updated: name, status (ENABLED/PAUSED), startTime/endTime, defaultBidAmount, cpaGoal, biddingStrategy (MANUAL_CPT or MAX_CONVERSIONS), automatedKeywordsOptIn, or targetingDimensions (all dimensions use { included: [...], excluded: [...] }, extra fields are forwarded to Apple). Returns the updated ad group. |
adgroups_delete |
params_unexplained no_return_description |
Permanently remove the ad group identified by campaignId (the campaign it belongs to) and adGroupId. Returns the deleted ad group. |
creatives_create |
params_unexplained no_return_description |
Create a creative that references a default product page, custom product page, or creative set; ads bind to creatives via creativeId. Provide name (a label for the creative), adamId (the app's Adam ID), type (CUSTOM_PRODUCT_PAGE, DEFAULT_PRODUCT_PAGE, or CREATIVE_SET), and productPageId when type=CUSTOM_PRODUCT_PAGE; extra passes additional fields through to Apple verbatim. Returns the created creative including its creativeId for use with ads_create. |
creatives_list |
name_restates_behavior no_return_description |
Return one page of creatives for the org at a time. Use limit (max 1000, Apple's hard cap) and offset to page through results, requesting with increasing offset until fewer than limit items are returned. Returns an array of creatives with their IDs, types, and product-page references. |
creatives_get |
params_unexplained no_return_description |
Return the full details of a single creative identified by creativeId (obtain IDs via creatives_list or creatives_find). |
creatives_find |
name_restates_behavior no_return_description |
Search for creatives org-wide using a selector: conditions filter on fields with operators like EQUALS, IN, or BETWEEN; fields selects which attributes to return; orderBy sorts results; pagination controls limit and offset. Returns the creatives matching the selector. |
ads_create |
params_unexplained no_return_description |
Create an ad inside the ad group adGroupId of campaign campaignId. The ad object requires name and creativeId (returned by creatives_create) and optionally creativeType and status (ENABLED/PAUSED); ads bind a creative to the ad group rather than directly to a CPP or creative set. Returns the created ad including its adId. |
ads_get |
params_unexplained no_return_description |
Return the full details of a single ad identified by adId within adGroupId of campaign campaignId. |
ads_list |
name_restates_behavior params_unexplained no_return_description |
Return one page of ads belonging to the ad group adGroupId in campaign campaignId at a time. Use limit (max 1000) and offset to page through results, requesting with increasing offset until fewer than limit items are returned. Returns an array of ads with their IDs, statuses, and creative bindings. |
ads_find_in_campaign |
name_restates_behavior params_unexplained no_return_description |
Search for ads across all ad groups in campaign campaignId using a selector: conditions filter on ad fields with operators like EQUALS, IN, or BETWEEN; fields selects returned attributes; orderBy sorts results; pagination controls limit and offset. Returns the ads matching the selector. |
ads_find_org_wide |
name_restates_behavior no_return_description |
Search for ads across the entire org using a selector: conditions filter on ad fields with operators like EQUALS, IN, or BETWEEN; fields selects returned attributes; orderBy sorts results; pagination controls limit and offset. Returns the ads matching the selector. |
ads_update |
params_unexplained no_return_description |
Update the ad identified by campaignId, adGroupId, and adId. The ad object carries the fields to change — typically status, name, or repointing to a different creativeId (create the new creative first via creatives_create). Returns the updated ad. |
ads_delete |
params_unexplained no_return_description |
Permanently remove the ad identified by campaignId, adGroupId, and adId. Returns the deleted ad. |
targeting_keywords_create |
name_restates_behavior params_unexplained no_return_description |
Bulk-add positive (targeting) keywords to the ad group adGroupId in campaign campaignId, up to 1000 keywords per call. Each keywords entry requires text (the phrase), matchType (BROAD matches close variants and synonyms; EXACT matches only the literal phrase), and bidAmount (per-keyword bid), with optional status. Returns the created keywords with their IDs. |
targeting_keywords_get |
params_unexplained no_return_description |
Return the full details of a single targeting keyword identified by keywordId within adGroupId of campaign campaignId. |
targeting_keywords_list |
name_restates_behavior params_unexplained no_return_description |
Return one page of targeting keywords on the ad group adGroupId in campaign campaignId at a time. Use limit (max 1000) and offset to page through results, requesting with increasing offset until fewer than limit items are returned. Returns an array of keywords with text, matchType, bid, and status. |
targeting_keywords_find |
params_unexplained no_return_description |
Search for targeting keywords across all ad groups in campaign campaignId using a selector (conditions, fields, orderBy, pagination). Apple offers no per-ad-group find endpoint, so filter by adGroupId in the selector conditions when needed. Returns the keywords matching the selector. |
targeting_keywords_update |
name_restates_behavior params_unexplained no_return_description |
Bulk-update existing targeting keywords on the ad group adGroupId in campaign campaignId, up to 1000 per call. Each keywords entry must include id (returned from a prior create/list call) plus the fields to change: text, matchType, bidAmount, or status. Returns the updated keywords. |
targeting_keywords_delete |
name_restates_behavior params_unexplained no_return_description |
Bulk-remove targeting keywords from the ad group adGroupId in campaign campaignId by passing keywordIds (up to 1000 per call). Returns the deleted keywords. |
targeting_keywords_delete_single |
name_restates_behavior params_unexplained no_return_description |
Remove a single targeting keyword identified by keywordId from the ad group adGroupId in campaign campaignId. Unlike the bulk variant, uses Apple's REST DELETE endpoint. Returns the deleted keyword. |
adgroup_negative_keywords_create |
name_restates_behavior params_unexplained no_return_description |
Bulk-add negative keywords to the ad group adGroupId in campaign campaignId, up to 1000 per call, to prevent ads from showing for unwanted searches. Each keywords entry requires text and matchType (BROAD or EXACT) with optional status. Returns the created keywords with their IDs. |
adgroup_negative_keywords_get |
params_unexplained no_return_description |
Return the full details of a single ad-group-level negative keyword identified by keywordId within adGroupId of campaign campaignId. |
adgroup_negative_keywords_list |
name_restates_behavior params_unexplained no_return_description |
Return one page of ad-group-level negative keywords on the ad group adGroupId in campaign campaignId at a time. Use limit (max 1000) and offset to page through results, requesting with increasing offset until fewer than limit items are returned. Returns an array of keywords with text, matchType, and status. |
adgroup_negative_keywords_find |
name_restates_behavior params_unexplained no_return_description |
Search for ad-group-level negative keywords across all ad groups in campaign campaignId using a selector (conditions, fields, orderBy, pagination). Returns the keywords matching the selector. |
adgroup_negative_keywords_update |
name_restates_behavior params_unexplained no_return_description |
Bulk-update ad-group-level negative keywords on the ad group adGroupId in campaign campaignId, up to 1000 per call. Each keywords entry must include id plus the fields to change: text, matchType, or status. Returns the updated keywords. |
adgroup_negative_keywords_delete |
name_restates_behavior params_unexplained no_return_description |
Bulk-delete up to 1000 ad-group-level negative keywords within a single ad group, scoped by campaignId and adGroupId; keywordIds lists the negative keyword IDs to remove. Returns the deleted negative keyword objects. |
campaign_negative_keywords_create |
name_restates_behavior params_unexplained no_return_description |
Bulk-create up to 1000 campaign-level negative keywords for the given campaignId. Each entry requires text plus a BROAD or EXACT matchType and optionally an ACTIVE/PAUSED status; campaign-level terms block matching search impressions across all ad groups in the campaign. Returns the created negative keyword objects. |
campaign_negative_keywords_get |
name_restates_behavior params_unexplained no_return_description |
Fetch a single campaign-level negative keyword by campaignId and keywordId to inspect its text, match type, and status. Returns the matching negative keyword object. |
campaign_negative_keywords_list |
name_restates_behavior params_unexplained no_return_description |
List campaign-level negative keywords for the given campaignId, paginated with limit (max 1000 per page) and offset. Returns a page of negative keyword objects along with pagination metadata. |
campaign_negative_keywords_find |
name_restates_behavior params_unexplained no_return_description |
Find campaign-level negative keywords within one campaign (campaignId) using a selector whose conditions (EQUALS, IN, CONTAINS, etc.) filter on fields, with fields/orderBy/pagination applied to the results. Returns the matching negative keyword records. |
campaign_negative_keywords_update |
name_restates_behavior params_unexplained no_return_description |
Bulk-update up to 1000 campaign-level negative keywords for the given campaignId; each entry must set the existing keyword id and may change text, matchType (BROAD/EXACT), or status (ACTIVE/PAUSED). Returns the updated negative keyword objects. |
campaign_negative_keywords_delete |
name_restates_behavior params_unexplained no_return_description |
Bulk-delete up to 1000 campaign-level negative keywords by their keywordIds for the given campaignId. Returns the deleted negative keyword objects. |
reports_campaigns |
params_unexplained |
Campaign-level performance report. Returns rows with impressions, taps, installs, spend, CPT/CPM/CPI, conversion rate, etc. Pass groupBy to pivot by country, device, age, gender. Set returnRecordsWithNoMetrics to include entities with zero activity in the period, returnRowTotals to add per-row subtotals, and returnGrandTotals to append an overall grand-total row. |
reports_adgroups |
name_restates_behavior params_unexplained no_return_description |
Ad-group-level performance report within a single campaign. Pass campaignId plus startTime/endTime and optionally groupBy to pivot rows; returns per-ad-group rows with metrics such as impressions, taps, and installs. Set returnRecordsWithNoMetrics to include inactive ad groups, returnRowTotals for subtotal rows, and returnGrandTotals for a grand-total row. |
reports_keywords_in_campaign |
name_restates_behavior params_unexplained no_return_description |
Keyword report rolled up across all ad groups in one campaign, over the requested time range. Pass campaignId and startTime/endTime; returns per-keyword rows with performance metrics like impressions, taps, and installs. Set returnRecordsWithNoMetrics to include zero-activity keywords, returnRowTotals for subtotal rows, and returnGrandTotals for a grand-total row. |
reports_keywords_in_adgroup |
params_unexplained no_return_description |
Keyword report scoped to a single ad group within a campaign (campaignId + adGroupId), over the requested time range. Returns per-keyword rows with performance metrics like impressions, taps, and installs. Set returnRecordsWithNoMetrics to include zero-activity keywords, returnRowTotals for subtotal rows, and returnGrandTotals for a grand-total row. |
reports_search_terms_in_campaign |
params_unexplained no_return_description |
Search-terms report rolled up across all ad groups in a campaign, over the requested time range. Returns rows of the actual queries users typed with per-term metrics such as impressions and taps - use it to harvest new keywords or negatives. Set returnRecordsWithNoMetrics to include terms with no activity, returnRowTotals for subtotal rows, and returnGrandTotals for a grand-total row. |
reports_search_terms_in_adgroup |
name_restates_behavior params_unexplained no_return_description |
Search-terms report scoped to a single ad group within a campaign (campaignId + adGroupId), over the requested time range. Returns rows of the actual queries users typed with per-term metrics such as impressions and taps. Set returnRecordsWithNoMetrics to include terms with no activity, returnRowTotals for subtotal rows, and returnGrandTotals for a grand-total row. |
reports_ads_in_campaign |
params_unexplained no_return_description |
Ad-level performance report, one row per Custom Product Page or creative variation, rolled up across the campaign over the requested time range. Returns rows with ad-level metrics such as impressions, taps, and spend. Set returnRecordsWithNoMetrics to include ads with no activity, returnRowTotals for subtotal rows, and returnGrandTotals for a grand-total row. |
custom_reports_create |
params_unexplained |
Create an Impression Share (Share of Voice) report asynchronously; returns a reportId you poll with custom_reports_get until state=COMPLETED. name labels the report (max 50 chars); granularity aggregates DAILY or WEEKLY (dateRange is needed when WEEKLY); selector filters to specific countryOrRegion or searchTerm values and controls fields, ordering, and pagination. |
custom_reports_get |
params_unexplained |
Fetch a single Impression Share report by reportId (the identifier returned by custom_reports_create). Poll the state until COMPLETED, then read the report rows exposing rank, impressionShare, lowImpressionShare, highImpressionShare, and searchPopularity. |
custom_reports_list |
no_return_description |
List all Impression Share reports created in the org, paginated with limit (max 1000) and offset. Returns report summaries including their reportId and current state so you can poll a completed one with custom_reports_get. |
budget_orders_create |
params_unexplained no_return_description |
Create a budget order (LOC accounts only), wrapping Apple's {bo, orgIds} envelope. name labels the order; budget holds the amount and 3-letter currency; startDate/endDate bound the flight (YYYY-MM-DD); supplySources selects where the budget applies (e.g. APPSTORE_SEARCH_RESULTS); billingEmail, primary buyer, clientName, and orderNumber are record-keeping fields; orgIds must contain exactly one org. Returns the created budget order with its ID. |
budget_orders_get |
name_restates_behavior params_unexplained no_return_description |
Fetch a single budget order by its budgetOrderId to inspect its budget, start/end dates, status, and buyer details. Returns the budget order object. |
budget_orders_list |
name_restates_behavior no_return_description |
List all budget orders in the org, paginated with limit (max 1000) and offset. Returns budget order summaries including their IDs and statuses so you can drill into a specific one with budget_orders_get. |
budget_orders_update |
params_unexplained no_return_description |
Update an existing budget order identified by budgetOrderId, passing the editable fields under bo (name, budget, dates, buyers, supplySources); status is read-only and v5 has no delete endpoint for budget orders. orgIds holds the single target org. Returns the updated budget order. |
product_page_reasons_find |
no_return_description |
Find ad-creative rejection reasons using a selector - filter on fields such as adamId, productPageId, assetGenId, countryOrRegion, languageCode, supplySource, or reasonLevel to audit why a creative was rejected by Apple's review team. Returns the matching rejection-reason records. |
product_page_reasons_get |
params_unexplained no_return_description |
Fetch a single ad-creative rejection reason by its productPageReasonId to see the explanation for a creative rejection. Returns the rejection-reason object. |
apple_search_ads_request |
params_unexplained no_return_description |
Escape hatch that calls any Apple Search Ads v5 endpoint with the given HTTP method (GET/POST/PUT/DELETE/PATCH) and path; authentication and org context are handled for you. method selects the HTTP verb, query adds query-string params, and body supplies the request payload when the endpoint needs one. Set noOrgContext for endpoints like /me or /acls. Returns the endpoint's raw JSON response. Use only if no dedicated tool covers the endpoint you need. |
Selection evidence
Confusable tool pairs.
10 pairs where similar names or overlapping descriptions may send an agent toward the wrong tool.
| Tool A | Tool B | Confidence | Why they collide |
|---|---|---|---|
targeting_keywords_delete |
targeting_keywords_delete_single |
high | A task like "delete the targeting keyword with id 42" fits both tools, as the bulk tool accepts a one-element list and the single tool takes one keywordId; nothing in the descriptions forces the single variant. |
adgroup_negative_keywords_delete |
campaign_negative_keywords_delete |
high | "Delete these negative keywords" is level-ambiguous; identical keyword text/id can exist at both ad-group and campaign level, so scope must be inferred from the task. |
adgroup_negative_keywords_get |
campaign_negative_keywords_get |
high | "Get/ fetch negative keyword 77" does not state whether it is ad-group-level or campaign-level; both tools share the same shape (campaignId + keywordId). |
adgroup_negative_keywords_list |
campaign_negative_keywords_list |
high | "List the negative keywords for this campaign" is ambiguous about level; without an adGroupId the agent may default to either listing tool and return the wrong scope. |
adgroup_negative_keywords_update |
campaign_negative_keywords_update |
high | "Pause/update negative keyword X" does not specify level; both are bulk updates keyed by campaignId and keyword id, so the agent can pick the wrong entity level. |
adgroup_negative_keywords_find |
campaign_negative_keywords_find |
medium | "Find negative keywords matching text X" is level-ambiguous and both accept a very similar selector; the task must mention adGroup vs campaign scope to disambiguate. |
reports_adgroups |
reports_keywords_in_adgroup |
medium | "Give me the performance report for ad group 123" fits both: reports_adgroups is the ad-group-level report (but has no adGroupId filter) while reports_keywords_in_adgroup is keyword-scoped and accepts adGroupId, so the agent can select the wrong granularity. |
reports_search_terms_in_campaign |
reports_search_terms_in_adgroup |
medium | A bare "search terms report" task is ambiguous about scope; both are named identically and share campaignId, differing only by an optional adGroupId that the task may omit. |
apps_locale_details |
cpp_locale_details |
medium | "Get the localized product page details for this app" is ambiguous between the app's default product page and a Custom Product Page; both key off adamId and differ mainly by productPageId. |
product_page_reasons_find |
product_page_reasons_get |
medium | "Get the rejection reason for this creative" fits both the selector-based find (which audits rejection reasons) and the single-by-Id get; the agent must decide whether a known reason id exists. |
Compare the field