Migrate to Asset-based Extensions

This document describes the migration and sunset process for Feed-based extensions. Feed-based extensions will be migrated to new Asset-based extensions in batches, and will fully sunset in early 2022.

Overview

The existing Feed-based extensions paradigm is deprecated in favor of Asset-based extensions. Asset-based extensions reduce the complexity of creating and managing extensions. Action is required in order to ensure that you maintain control over your ad extensions.

The term "Feed-based extension" refers to any of the resources or functions associated with the following services:

While Feed-based extensions and Asset-based extensions will coexist for a time, we strongly recommend using Asset-based extensions as soon as is feasible.

If you do have active extensions of both types, you may see a notification on the Extensions page in the Google Ads UI, similar to the screenshot below. "Legacy" extensions are Feed-based extensions, while "Upgraded" extensions are Asset-based.

Upgraded promotion screen

Note that once you have created any Asset-based extensions, the view will default to "Upgraded" extensions.

Migration details

Feeds are migrated by copying their existing values into a new Asset-based extension instance. Existing Feed-based extensions will be automatically copied into Asset-based extensions on specific dates. All Asset-based extensions will have new, unique ID values.

Migration schedule

All existing Feed-based extensions will automatically migrate to Asset-based extensions in batches:

Extension Types Auto Migration Date Sunset Date
Callout
Promotion
Sitelink
Structured Snippet
October 20, 2021 February 15, 2022
App
Call
Hotel Callout
Price
February 15, 2022 April 8, 2022

All CREATE and MUTATE actions will be disabled for the relevant extension types after each auto migration date. You can opt out specific account IDs from the first batch automatic migration (callout, promotion, sitelink, and structured snippet). However, note that these extensions will become unavailable for all accounts on February 15, 2022. See this recent blog post entry for guidance on how to opt out.

Reporting for Feed-based extensions will be available until August 2022.

The automatic migration process will create Asset-based extensions equivalent to the current Feed-based extensions and replace all occurrences of AdGroupFeed, CampaignFeed, and CustomerFeed with equivalent AdGroupAsset, CampaignAsset, and CustomerAsset. If the ad group, campaign, or customer has already been attached to an Asset, the corresponding Feed will be ignored.

Changes in Asset-based extensions

While Asset-based extensions provide the same functionality as the existing Feed-based extensions, some existing features have been removed, including matching functions and most targeting fields.

For targeting, the ad group, campaign, keyword, location, and mobile fields will be unavailable to Asset-based extensions. Existing ad group, campaign, and mobile targeting will be honored during the automatic migration. Start time, end time, and ad schedules will be unavailable for the Asset-based Structured Snippet extension, and will not be automatically migrated.

For Asset-based extensions going forward, we recommend using targeting features already available at the campaign and ad group level. For mobile preference, consider including mobile-friendly URLs for any links.

Matching functions will not be available to Asset-based extensions. During the automatic migration, only the following matching functions will be honored:

Matching Function Migration Notes
EQUALS(FEED_ITEM_ID, 1234567) Migrated as-is
IN(FEED_ITEM_ID,{1234567,1234568,1234569}) Migrated as-is
IDENTITY(false) Migrated as excluded_parent_asset_field_types
IDENTITY(true) Migrated with all linked feed items
AND(
IN(FEED_ITEM_ID,{1234567,1234568,1234569}),
EQUALS(CONTEXT.DEVICE,"Mobile"))
Migrated as IN(FEED_ITEM_ID,{1234567,1234568,1234569})
AND(
EQUALS(FEED_ITEM_ID, 1234567),
EQUALS(CONTEXT.DEVICE,"Mobile"))
Migrated as EQUALS(FEED_ITEM_ID,1234567)

Tracking Asset-based extensions using ValueTrack parameters in URLs

All ValueTrack parameters will continue to function when used in URLs in Asset-based extensions except for {feeditemid}. Instead, use {extensionid} to link any final URL, tracking template, or custom parameter to an Asset-based extension. The {feeditemid} parameter will no longer return any value once the Feed-based extension is migrated to an Asset-based extension.

The automatic migration procedure will not change occurrences of {feeditemid} into {extensionid} for you; you must update any URLs that utilize the {feeditemid} parameter to also include the {extensionid} parameter. Doing so will ensure that you are tracking clicks to either type of extension. Note, however, that only one of these fields will be populated when a click occurs.

Migration procedure

To migrate a Feed-based extension to an Asset-based extension, take the following actions:

  1. Identify an existing Feed-based extension that you intend to continue to use.
  2. Copy the Feed-based extension's contents into a new instance of the corresponding Asset-based extension. For example, the contents of an ExtensionFeedItem of type Promotion would be copied into a new PromotionAsset object.
  3. Associate the new Asset-based extension with the same ad groups and campaigns.
  4. Verify that the Asset-based extension is correctly configured.
  5. Remove the Feed-based extension from your account.

An extension type is fully migrated when all feed items with a particular PlaceholderType have been removed. See removing extensions for guidance.

The following example explains each step of the above process with a promotion extension.

Identify Feed-based extensions

Use the following Google Ads Query Language query to request active extension feed item IDs of a particular extension type.

SELECT extension_feed_item.id
FROM extension_feed_item
WHERE
  extension_feed_item.status = 'ENABLED'
  AND extension_feed_item.extension_type = 'PROMOTION'

The IDs returned can then be used to fetch extension details.

Fetch Feed-based extension details

Once you have identified a Feed-based extension to migrate, request its full details using its ID with another Google Ads Query Language query.

SELECT
  extension_feed_item.id,
  extension_feed_item.ad_schedules,
  extension_feed_item.device,
  extension_feed_item.status,
  extension_feed_item.start_date_time,
  extension_feed_item.end_date_time,
  extension_feed_item.targeted_campaign,
  extension_feed_item.targeted_ad_group,
  extension_feed_item.promotion_feed_item.discount_modifier,
  extension_feed_item.promotion_feed_item.final_mobile_urls,
  extension_feed_item.promotion_feed_item.final_url_suffix,
  extension_feed_item.promotion_feed_item.final_urls,
  extension_feed_item.promotion_feed_item.language_code,
  extension_feed_item.promotion_feed_item.money_amount_off.amount_micros,
  extension_feed_item.promotion_feed_item.money_amount_off.currency_code,
  extension_feed_item.promotion_feed_item.occasion,
  extension_feed_item.promotion_feed_item.orders_over_amount.amount_micros,
  extension_feed_item.promotion_feed_item.orders_over_amount.currency_code,
  extension_feed_item.promotion_feed_item.percent_off,
  extension_feed_item.promotion_feed_item.promotion_code,
  extension_feed_item.promotion_feed_item.promotion_end_date,
  extension_feed_item.promotion_feed_item.promotion_start_date,
  extension_feed_item.promotion_feed_item.promotion_target,
  extension_feed_item.promotion_feed_item.tracking_url_template
FROM extension_feed_item
WHERE extension_feed_item.extension_type = 'PROMOTION'
  AND extension_feed_item.id = 123456789012
LIMIT 1

The returned ExtensionFeedItem will have all set values currently contained in the specified Feed-based extension that can be applied to an Asset-based extension.

If using URL custom parameters, you will need to fetch them using an additional Google Ads Query Language query. Add any results to the ExtensionFeedItem instance retrieved by the previous query. Note that the feed item ID in this query is the same as the extension feed item ID used in the previous query.

SELECT feed_item.url_custom_parameters
FROM feed_item
WHERE feed_item.id = 123456789012

Create an Asset-based extension

The values fetched in the previous step can then be applied to a new instance of the relevant asset.

Java

private String createPromotionAssetFromFeed(
    GoogleAdsClient googleAdsClient, Long customerId, ExtensionFeedItem extensionFeedItem) {
  PromotionFeedItem promotionFeedItem = extensionFeedItem.getPromotionFeedItem();
  // Creates the Promotion asset.
  Asset.Builder asset =
      Asset.newBuilder()
          .setName("Migrated from feed item " + extensionFeedItem.getId())
          .setTrackingUrlTemplate(promotionFeedItem.getTrackingUrlTemplate())
          .setFinalUrlSuffix(promotionFeedItem.getFinalUrlSuffix())
          .setPromotionAsset(
              PromotionAsset.newBuilder()
                  .setPromotionTarget(promotionFeedItem.getPromotionTarget())
                  .setDiscountModifier(promotionFeedItem.getDiscountModifier())
                  .setRedemptionEndDate(promotionFeedItem.getPromotionStartDate())
                  .setRedemptionEndDate(promotionFeedItem.getPromotionEndDate())
                  .setOccasion(promotionFeedItem.getOccasion())
                  .setLanguageCode(promotionFeedItem.getLanguageCode())
                  .addAllAdScheduleTargets(extensionFeedItem.getAdSchedulesList()))
          .addAllFinalUrls(promotionFeedItem.getFinalUrlsList())
          .addAllFinalMobileUrls(promotionFeedItem.getFinalMobileUrlsList())
          .addAllUrlCustomParameters(promotionFeedItem.getUrlCustomParametersList());

  // Either PercentOff or MoneyAmountOff must be set.
  if (promotionFeedItem.getPercentOff() > 0) {
    // Adjusts the percent off scale when copying.
    asset.getPromotionAssetBuilder().setPercentOff(promotionFeedItem.getPercentOff() / 100);
  } else {
    asset.getPromotionAssetBuilder().setMoneyAmountOff(promotionFeedItem.getMoneyAmountOff());
  }
  // Either PromotionCode or OrdersOverAmount must be set.
  if (promotionFeedItem.getPromotionCode() != null
      && promotionFeedItem.getPromotionCode().length() > 0) {
    asset.getPromotionAssetBuilder().setPromotionCode(promotionFeedItem.getPromotionCode());
  } else {
    asset.getPromotionAssetBuilder().setOrdersOverAmount(promotionFeedItem.getOrdersOverAmount());
  }
  // Sets the start and end dates if set in the existing extension.
  if (extensionFeedItem.hasStartDateTime()) {
    asset.getPromotionAssetBuilder().setStartDate(extensionFeedItem.getStartDateTime());
  }
  if (extensionFeedItem.hasEndDateTime()) {
    asset.getPromotionAssetBuilder().setEndDate(extensionFeedItem.getEndDateTime());
  }
  // Builds an operation to create the Promotion asset.
  AssetOperation operation = AssetOperation.newBuilder().setCreate(asset).build();
  // Gets the Asset Service client.
  try (AssetServiceClient assetServiceClient =
      googleAdsClient.getLatestVersion().createAssetServiceClient()) {
    // Issues the request and returns the resource name of the new Promotion asset.
    MutateAssetsResponse response =
        assetServiceClient.mutateAssets(String.valueOf(customerId), ImmutableList.of(operation));
    String resourceName = response.getResults(0).getResourceName();
    System.out.println("Created Promotion asset with resource name " + resourceName);
    return resourceName;
  }
}
      

C#

private string CreatePromotionAssetFromFeed(GoogleAdsClient client, long customerId,
    ExtensionFeedItem extensionFeedItem)
{
    // Get the Asset Service client.
    AssetServiceClient assetServiceClient = client.GetService(Services.V8.AssetService);

    PromotionFeedItem promotionFeedItem = extensionFeedItem.PromotionFeedItem;

    // Create the Promotion asset.
    Asset asset = new Asset
    {
        // Name field is optional.
        Name = $"Migrated from feed item #{extensionFeedItem.Id}",
        PromotionAsset = new PromotionAsset
        {
            PromotionTarget = promotionFeedItem.PromotionTarget,
            DiscountModifier = promotionFeedItem.DiscountModifier,
            RedemptionStartDate = promotionFeedItem.PromotionStartDate,
            RedemptionEndDate = promotionFeedItem.PromotionEndDate,
            Occasion = promotionFeedItem.Occasion,
            LanguageCode = promotionFeedItem.LanguageCode,
        },
        TrackingUrlTemplate = promotionFeedItem.TrackingUrlTemplate,
        FinalUrlSuffix = promotionFeedItem.FinalUrlSuffix
    };

    // Either PercentOff or MoneyAmountOff must be set.
    if (promotionFeedItem.PercentOff > 0)
    {
        // Adjust the percent off scale when copying.
        asset.PromotionAsset.PercentOff = promotionFeedItem.PercentOff / 100;
    }
    else
    {
        asset.PromotionAsset.MoneyAmountOff = new Money
        {
            AmountMicros = promotionFeedItem.MoneyAmountOff.AmountMicros,
            CurrencyCode = promotionFeedItem.MoneyAmountOff.CurrencyCode
        };
    }

    // Either PromotionCode or OrdersOverAmount must be set.
    if (!string.IsNullOrEmpty(promotionFeedItem.PromotionCode))
    {
        asset.PromotionAsset.PromotionCode = promotionFeedItem.PromotionCode;
    }
    else
    {
        asset.PromotionAsset.OrdersOverAmount = new Money
        {
            AmountMicros = promotionFeedItem.OrdersOverAmount.AmountMicros,
            CurrencyCode = promotionFeedItem.OrdersOverAmount.CurrencyCode
        };
    }

    // Set the start and end dates if set in the existing extension.
    if (extensionFeedItem.HasStartDateTime)
    {
        asset.PromotionAsset.StartDate = DateTime.Parse(extensionFeedItem.StartDateTime)
            .ToString("yyyy-MM-dd");
    }

    if (extensionFeedItem.HasEndDateTime)
    {
        asset.PromotionAsset.EndDate = DateTime.Parse(extensionFeedItem.EndDateTime)
            .ToString("yyyy-MM-dd");
    }

    asset.PromotionAsset.AdScheduleTargets.Add(extensionFeedItem.AdSchedules);
    asset.FinalUrls.Add(promotionFeedItem.FinalUrls);
    asset.FinalMobileUrls.Add(promotionFeedItem.FinalMobileUrls);
    asset.UrlCustomParameters.Add(promotionFeedItem.UrlCustomParameters);

    // Build an operation to create the Promotion asset.
    AssetOperation operation = new AssetOperation
    {
        Create = asset
    };

    // Issue the request and return the resource name of the new Promotion asset.
    MutateAssetsResponse response = assetServiceClient.MutateAssets(
        customerId.ToString(), new[] { operation });
    Console.WriteLine("Created Promotion asset with resource name " +
        $"{response.Results.First().ResourceName}");
    return response.Results.First().ResourceName;
}
      

PHP

private static function createPromotionAssetFromFeed(
    GoogleAdsClient $googleAdsClient,
    int $customerId,
    ExtensionFeedItem $extensionFeedItem
): string {
    $promotionFeedItem = $extensionFeedItem->getPromotionFeedItem();
    // Creates the Promotion asset.
    $asset = new Asset([
        // Name field is optional.
        'name' => 'Migrated from feed item #' . $extensionFeedItem->getId(),
        'promotion_asset' => new PromotionAsset([
            'promotion_target' => $promotionFeedItem->getPromotionTarget(),
            'discount_modifier' => $promotionFeedItem->getDiscountModifier(),
            'redemption_start_date' => $promotionFeedItem->getPromotionStartDate(),
            'redemption_end_date' => $promotionFeedItem->getPromotionEndDate(),
            'occasion' => $promotionFeedItem->getOccasion(),
            'language_code' => $promotionFeedItem->getLanguageCode(),
            'ad_schedule_targets' => $extensionFeedItem->getAdSchedules()
        ]),
        'tracking_url_template' => $promotionFeedItem->getTrackingUrlTemplate(),
        'url_custom_parameters' => $promotionFeedItem->getUrlCustomParameters(),
        'final_urls' => $promotionFeedItem->getFinalUrls(),
        'final_mobile_urls' => $promotionFeedItem->getFinalMobileUrls(),
        'final_url_suffix' => $promotionFeedItem->getFinalUrlSuffix(),
    ]);

    // Either percent off or money amount off must be set.
    if ($promotionFeedItem->getPercentOff() > 0) {
        // Adjust the percent off scale when copying.
        $asset->getPromotionAsset()->setPercentOff($promotionFeedItem->getPercentOff() / 100);
    } else {
        $money = new Money([
           'amount_micros' => $promotionFeedItem->getMoneyAmountOff()->getAmountMicros(),
           'currency_code' => $promotionFeedItem->getMoneyAmountOff()->getCurrencyCode()
        ]);
        $asset->getPromotionAsset()->setMoneyAmountOff($money);
    }

    // Either promotion code or orders over amount must be set.
    if (!empty($promotionFeedItem->getPromotionCode())) {
        $asset->getPromotionAsset()->setPromotionCode($promotionFeedItem->getPromotionCode());
    } else {
        $money = new Money([
            'amount_micros' => $promotionFeedItem->getOrdersOverAmount()->getAmountMicros(),
            'currency_code' => $promotionFeedItem->getOrdersOverAmount()->getCurrencyCode()
        ]);
        $asset->getPromotionAsset()->setOrdersOverAmount($money);
    }

    if ($extensionFeedItem->hasStartDateTime()) {
        $startDateTime = new \DateTime($extensionFeedItem->getStartDateTime());
        $asset->getPromotionAsset()->setStartDate($startDateTime->format('yyyy-MM-dd'));
    }
    if ($extensionFeedItem->hasEndDateTime()) {
        $endDateTime = new \DateTime($extensionFeedItem->getEndDateTime());
        $asset->getPromotionAsset()->setEndDate($endDateTime->format('yyyy-MM-dd'));
    }

    // Creates an operation to add the Promotion asset.
    $assetOperation = new AssetOperation();
    $assetOperation->setCreate($asset);

    // Issues a mutate request to add the Promotion asset and prints its information.
    $assetServiceClient = $googleAdsClient->getAssetServiceClient();
    $response = $assetServiceClient->mutateAssets($customerId, [$assetOperation]);
    $assetResourceName = $response->getResults()[0]->getResourceName();
    printf(
        "Created the Promotion asset with resource name: '%s'.%s",
        $assetResourceName,
        PHP_EOL
    );

    return $assetResourceName;
}
      

Python

def _create_promotion_asset_from_feed(client, customer_id, extension_feed_item):
    """Retrieves all campaigns associated with the given FeedItem resource name.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        extension_feed_item: an extension feed item.

    Returns:
        the resource name of a newly created promotion asset.
    """
    asset_service = client.get_service("AssetService")
    promotion_feed_item = extension_feed_item.promotion_feed_item

    # Create an asset operation to start building the new promotion asset using
    # data from the given extension feed item.
    asset_operation = client.get_type("AssetOperation")
    asset = asset_operation.create
    asset.name = f"Migrated from feed item ID '{extension_feed_item.id}'"
    asset.tracking_url_template = promotion_feed_item.tracking_url_template
    asset.final_url_suffix = promotion_feed_item.final_url_suffix
    asset.final_urls.extend(promotion_feed_item.final_urls)
    asset.final_mobile_urls.extend(promotion_feed_item.final_mobile_urls)

    promotion_asset = asset.promotion_asset
    promotion_asset.promotion_target = promotion_feed_item.promotion_target
    promotion_asset.discount_modifier = promotion_feed_item.discount_modifier
    promotion_asset.redemption_start_date = (
        promotion_feed_item.promotion_start_date
    )
    promotion_asset.redemption_end_date = promotion_feed_item.promotion_end_date
    promotion_asset.occasion = promotion_feed_item.occasion
    promotion_asset.language_code = promotion_feed_item.language_code
    promotion_asset.ad_schedule_targets.extend(extension_feed_item.ad_schedules)

    # Either percent_off or money_amount_off must be set.
    if promotion_feed_item.percent_off > 0:
        # Adjust the percent off scale after copying. Extension feed items
        # interpret 1,000,000 as 1% and assets interpret 1,000,000 as 100% so
        # to migrate the correct discount value we must divide it by 100.
        promotion_asset.percent_off = int(promotion_feed_item.percent_off / 100)
    else:
        # If percent_off is not set then copy money_amount_off. This field is
        # an instance of Money in both cases, so setting the field with
        # copy_from is possible. Using regular assignment is also valid here.
        client.copy_from(
            promotion_asset.money_amount_off,
            promotion_feed_item.money_amount_off,
        )

    # Check if promotion_code field is set
    if promotion_feed_item.promotion_code:
        promotion_asset.promotion_code = promotion_feed_item.promotion_code
    else:
        # If promotion_code is not set then copy orders_over_amount. This field
        # is an instance of Money in both cases, so setting the field with
        # copy_from is possible. Using regular assignment is also valid here.
        client.copy_from(
            promotion_asset.orders_over_amount,
            promotion_feed_item.orders_over_amount,
        )

    # Set the start and end dates if set in the existing extension.
    if promotion_feed_item.promotion_start_date:
        promotion_asset.start_date = promotion_feed_item.promotion_start_date

    if promotion_feed_item.promotion_end_date:
        promotion_asset.end_date = promotion_feed_item.promotion_end_date

    response = asset_service.mutate_assets(
        customer_id=customer_id, operations=[asset_operation]
    )
    resource_name = response.results[0].resource_name
    print(f"Created promotion asset with resource name: '{resource_name}'")

    return resource_name


      

Ruby

def create_promotion_asset_from_feed(client, customer_id, extension_feed_item)
  # Create a Promotion asset that copies values from the specified extension feed item.

  asset_service = client.service.asset
  promotion_feed_item = extension_feed_item.promotion_feed_item

  # Create an asset operation to start building the new promotion asset using
  # data from the given extension feed item.
  asset_operation = client.operation.create_resource.asset do |asset|
    asset.name = "Migrated from feed item ID '#{extension_feed_item.id}'"
    asset.tracking_url_template = promotion_feed_item.tracking_url_template
    asset.final_url_suffix = promotion_feed_item.final_url_suffix
    asset.final_urls += promotion_feed_item.final_urls
    asset.final_mobile_urls += promotion_feed_item.final_mobile_urls

    # Create the Promotion asset.
    asset.promotion_asset = client.resource.promotion_asset do |pa|
      pa.promotion_target = promotion_feed_item.promotion_target
      pa.discount_modifier = promotion_feed_item.discount_modifier
      pa.redemption_start_date = promotion_feed_item.promotion_start_date
      pa.redemption_end_date = promotion_feed_item.promotion_end_date
      pa.occasion = promotion_feed_item.occasion
      pa.language_code = promotion_feed_item.language_code
      pa.ad_schedule_targets += extension_feed_item.ad_schedules

      # Either percent_off or money_amount_off must be set.
      if promotion_feed_item.percent_off.positive?
        # Adjust the percent off scale after copying.
        pa.percent_off = int(promotion_feed_item.percent_off / 100)
      else
        # If percent_off is not set then copy money_amount_off. This field is
        # an instance of Money in both cases, so setting the field with
        # copy_from is possible. Using regular assignment is also valid here.
        pa.money_amount_off = promotion_feed_item.money_amount_off
      end

      # Either promotion_code or orders_over_amount must be set.
      if promotion_feed_item.promotion_code.empty?
        pa.orders_over_amount = promotion_feed_item.orders_over_amount
      else
        pa.promotion_code = promotion_feed_item.promotion_code
      end

      # Set the start and end dates if set in the existing extension.
      unless promotion_feed_item.promotion_start_date.empty?
        pa.start_date = promotion_feed_item.promotion_start_date
      end

      unless promotion_feed_item.promotion_end_date.empty?
        pa.end_date = promotion_feed_item.promotion_end_date
      end
    end
  end

  response = asset_service.mutate_assets(customer_id: customer_id, operations: [asset_operation])
  resource_name = response.results.first.resource_name
  puts "Created promotion asset with resource name: '#{resource_name}'"

  resource_name
end
      

Perl

sub create_promotion_asset_from_feed {
  my ($api_client, $customer_id, $extension_feed_item) = @_;

  my $promotion_feed_item = $extension_feed_item->{promotionFeedItem};

  # Create the Promotion asset.
  my $asset = Google::Ads::GoogleAds::V8::Resources::Asset->new({
      name => "Migrated from feed item #" . $extension_feed_item->{id},
      trackingUrlTemplate => $promotion_feed_item->{trackingUrlTemplate},
      finalUrlSuffix      => $promotion_feed_item->{finalUrlSuffix},
      promotionAsset => Google::Ads::GoogleAds::V8::Common::PromotionAsset->new(
        {
          promotionTarget     => $promotion_feed_item->{promotionTarget},
          discountModifier    => $promotion_feed_item->{discountModifier},
          redemptionStartDate => $promotion_feed_item->{promotionStartDate},
          redemptionEndDate   => $promotion_feed_item->{promotionEndDate},
          occasion            => $promotion_feed_item->{occasion},
          languageCode        => $promotion_feed_item->{languageCode}})});

  push @{$asset->{finalUrls}}, @{$promotion_feed_item->{finalUrls}};

  # Copy optional fields if present in the existing extension.
  if (defined($extension_feed_item->{adSchedules})) {
    push @{$asset->{promotionAsset}{adScheduleTargets}},
      @{$extension_feed_item->{adSchedules}};
  }

  if (defined($promotion_feed_item->{finalMobileUrls})) {
    push @{$asset->{finalMobileUrls}},
      @{$promotion_feed_item->{finalMobileUrls}};
  }

  if (defined($promotion_feed_item->{urlCustomParameters})) {
    push @{$asset->{urlCustomParameters}},
      @{$promotion_feed_item->{urlCustomParameters}};
  }

  # Either percentOff or moneyAmountOff must be set.
  if (defined($promotion_feed_item->{percentOff})) {
    # Adjust the percent off scale when copying.
    $asset->{promotionAsset}{percentOff} =
      $promotion_feed_item->{percentOff} / 100;
  } else {
    $asset->{promotionAsset}{moneyAmountOff} =
      Google::Ads::GoogleAds::V8::Common::Money->new({
        amountMicros => $promotion_feed_item->{moneyAmountOff}{amountMicros},
        currencyCode => $promotion_feed_item->{moneyAmountOff}{currencyCode}});
  }

  # Either promotionCode or ordersOverAmount must be set.
  if (defined($promotion_feed_item->{promotionCode})) {
    $asset->{promotionAsset}{promotionCode} =
      $promotion_feed_item->{promotionCode};
  } else {
    $asset->{promotionAsset}{ordersOverAmount} =
      Google::Ads::GoogleAds::V8::Common::Money->new({
        amountMicros => $promotion_feed_item->{ordersOverAmount}{amountMicros},
        currencyCode => $promotion_feed_item->{ordersOverAmount}{currencyCode}}
      );
  }

  # Set the start and end dates if set in the existing extension.
  if (defined($extension_feed_item->{startDateTime})) {
    $asset->{promotionAsset}{startDate} =
      substr($extension_feed_item->{startDateTime},
      0, index($extension_feed_item->{startDateTime}, ' '));
  }

  if (defined($extension_feed_item->{endDateTime})) {
    $asset->{promotionAsset}{endDate} =
      substr($extension_feed_item->{endDateTime},
      0, index($extension_feed_item->{endDateTime}, ' '));
  }

  # Build an operation to create the Promotion asset.
  my $operation =
    Google::Ads::GoogleAds::V8::Services::AssetService::AssetOperation->new({
      create => $asset
    });

  # Issue the request and return the resource name of the new Promotion asset.
  my $response = $api_client->AssetService()->mutate({
      customerId => $customer_id,
      operations => [$operation]});

  printf
    "Created Promotion asset with resource name '%s'.\n",
    $response->{results}[0]{resourceName};

  return $response->{results}[0]{resourceName};
}
      

Associate the asset with campaigns and ad groups

The new asset can now be associated with the same customers, campaigns, and ad groups as the original Feed-based extension. First, fetch the resource IDs that are currently associated with the extension setting.

Java

private List<Long> getTargetedCampaignIds(
    GoogleAdsServiceClient client, Long customerId, String extensionFeedItemResourceName) {
  String query =
      "SELECT campaign.id, campaign_extension_setting.extension_feed_items "
          + "FROM campaign_extension_setting "
          + "WHERE campaign_extension_setting.extension_type = 'PROMOTION' "
          + "  AND campaign.status != 'REMOVED'";
  ServerStream<SearchGoogleAdsStreamResponse> serverStream =
      client
          .searchStreamCallable()
          .call(
              SearchGoogleAdsStreamRequest.newBuilder()
                  .setCustomerId(String.valueOf(customerId))
                  .setQuery(query)
                  .build());
  List<Long> campaignIds = new ArrayList<>();
  for (SearchGoogleAdsStreamResponse response : serverStream) {
    for (GoogleAdsRow row : response.getResultsList()) {
      Campaign campaign = row.getCampaign();
      CampaignExtensionSetting extensionSetting = row.getCampaignExtensionSetting();
      // Adds the campaign ID to the list of IDs if the extension feed item is
      // associated with this extension setting.
      if (extensionSetting.getExtensionFeedItemsList().contains(extensionFeedItemResourceName)) {
        campaignIds.add(campaign.getId());
        System.out.println("Found matching campaign with ID " + campaign.getId());
      }
    }
  }
  return campaignIds;
}
      

C#

private List<long> GetTargetedCampaignIds(GoogleAdsServiceClient googleAdsServiceClient,
    long customerId, string extensionFeedResourceName)
{
    List<long> campaignIds = new List<long>();

    string query = @"
        SELECT campaign.id, campaign_extension_setting.extension_feed_items
        FROM campaign_extension_setting
        WHERE campaign_extension_setting.extension_type = 'PROMOTION'
          AND campaign.status != 'REMOVED'";

    googleAdsServiceClient.SearchStream(customerId.ToString(), query,
        delegate (SearchGoogleAdsStreamResponse response)
        {
            foreach (GoogleAdsRow googleAdsRow in response.Results)
            {
                // Add the campaign ID to the list of IDs if the extension feed item is
                // associated with this extension setting.
                if (googleAdsRow.CampaignExtensionSetting.ExtensionFeedItems.Contains(
                    extensionFeedResourceName))
                {
                    Console.WriteLine(
                        $"Found matching campaign with ID {googleAdsRow.Campaign.Id}.");
                    campaignIds.Add(googleAdsRow.Campaign.Id);
                }
            }
        }
    );

    return campaignIds;
}
      

PHP

private static function getTargetedCampaignIds(
    GoogleAdsClient $googleAdsClient,
    int $customerId,
    string $extensionFeedItemResourceName
): array {
    $campaignIds = [];
    $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient();
    // Create a query that will retrieve the campaign extension settings.
    $query = "SELECT campaign.id, campaign_extension_setting.extension_feed_items "
        . "FROM campaign_extension_setting "
        . "WHERE campaign_extension_setting.extension_type = 'PROMOTION' "
        . "AND campaign.status != 'REMOVED'";

    // Issue a search request to get the campaign extension settings.
    /** @var GoogleAdsServerStreamDecorator $stream */
    $stream = $googleAdsServiceClient->searchStream($customerId, $query);
    foreach ($stream->iterateAllElements() as $googleAdsRow) {
        /** @var GoogleAdsRow $googleAdsRow */
        // Add the campaign ID to the list of IDs if the extension feed item is
        // associated with this extension setting.
        if (
            in_array(
                $extensionFeedItemResourceName,
                iterator_to_array(
                    $googleAdsRow->getCampaignExtensionSetting()->getExtensionFeedItems()
                )
            )
        ) {
            printf(
                "Found matching campaign with ID %d.%s",
                $googleAdsRow->getCampaign()->getId(),
                PHP_EOL
            );
            $campaignIds[] = $googleAdsRow->getCampaign()->getId();
        }
    }
    return $campaignIds;
}
      

Python

def _get_targeted_campaign_ids(client, customer_id, resource_name):
    """Retrieves all campaigns associated with the given FeedItem resource name.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        resource_name: an extension feed item resource name.

    Returns:
        a list of campaign IDs.
    """
    ga_service = client.get_service("GoogleAdsService")

    query = """
      SELECT
        campaign.id,
        campaign_extension_setting.extension_feed_items
      FROM campaign_extension_setting
      WHERE
        campaign_extension_setting.extension_type = 'PROMOTION'
        AND campaign.status != 'REMOVED'"""

    stream = ga_service.search_stream(customer_id=customer_id, query=query)

    campaign_ids = []

    for batch in stream:
        for row in batch.results:
            feed_items = row.campaign_extension_setting.extension_feed_items
            if resource_name in feed_items:
                print(f"Found matching campaign with ID: '{row.campaign.id}'")
                campaign_ids.append(row.campaign.id)

    return campaign_ids


      

Ruby

def get_targeted_campaign_ids(client, customer_id, resource_name)
  # Finds and returns all of the campaigns that are associated with the specified
  # Promotion extension feed item.

  query = <<~QUERY
    SELECT
      campaign.id,
      campaign_extension_setting.extension_feed_items
    FROM campaign_extension_setting
    WHERE
      campaign_extension_setting.extension_type = 'PROMOTION'
      AND campaign.status != 'REMOVED'
  QUERY

  responses = client.service.google_ads.search_stream(customer_id: customer_id, query: query)

  campaign_ids = []

  responses.each do |response|
    response.results.each do |row|
      feed_items = row.campaign_extension_setting.extension_feed_items
      if feed_items.include?(resource_name)
        puts "Found matching campaign with ID '#{row.campaign.id}'."
        campaign_ids << row.campaign.id
      end
    end
  end

  campaign_ids
end
      

Perl

sub get_targeted_campaign_ids {
  my ($google_ads_service, $customer_id, $extension_feed_item_resource_name) =
    @_;

  my @campaign_ids;

  my $query = "
        SELECT campaign.id, campaign_extension_setting.extension_feed_items
        FROM campaign_extension_setting
        WHERE campaign_extension_setting.extension_type = 'PROMOTION'
          AND campaign.status != 'REMOVED'";

  my $search_stream_request =
    Google::Ads::GoogleAds::V8::Services::GoogleAdsService::SearchGoogleAdsStreamRequest
    ->new({
      customerId => $customer_id,
      query      => $query
    });

  my $search_stream_handler =
    Google::Ads::GoogleAds::Utils::SearchStreamHandler->new({
      service => $google_ads_service,
      request => $search_stream_request
    });

  $search_stream_handler->process_contents(
    sub {
      my $google_ads_row = shift;

      # Add the campaign ID to the list of IDs if the extension feed item
      # is associated with this extension setting.
      if (grep { $_ eq $extension_feed_item_resource_name }
        @{$google_ads_row->{campaignExtensionSetting}{extensionFeedItems}})
      {
        printf
          "Found matching campaign with ID $google_ads_row->{campaign}{id}.\n";
        push @campaign_ids, $google_ads_row->{campaign}{id};
      }
    });

  return @campaign_ids;
}
      

Then, create and upload a new CampaignAsset, AdGroupAsset, or CustomerAsset to link the asset with each resource.

Java

private void associateAssetWithCampaigns(
    GoogleAdsClient googleAdsClient,
    Long customerId,
    String promotionAssetResourceName,
    List<Long> campaignIds) {
  if (campaignIds.isEmpty()) {
    System.out.println("Asset was not associated with any campaigns.");
    return;
  }

  // Constructs an operation to associate the asset with each campaign.
  List<CampaignAssetOperation> campaignAssetOperations =
      campaignIds.stream()
          .map(
              id ->
                  CampaignAssetOperation.newBuilder()
                      .setCreate(
                          CampaignAsset.newBuilder()
                              .setAsset(promotionAssetResourceName)
                              .setFieldType(AssetFieldType.PROMOTION)
                              .setCampaign(ResourceNames.campaign(customerId, id)))
                      .build())
          .collect(Collectors.toList());

  // Creates a service client.
  try (CampaignAssetServiceClient campaignAssetServiceClient =
      googleAdsClient.getLatestVersion().createCampaignAssetServiceClient()) {
    // Issues the mutate request.
    MutateCampaignAssetsResponse response =
        campaignAssetServiceClient.mutateCampaignAssets(
            String.valueOf(customerId), campaignAssetOperations);
    // Prints some information about the result.
    for (MutateCampaignAssetResult result : response.getResultsList()) {
      System.out.println("Created campaign asset with resource name " + result.getResourceName());
    }
  }
}
      

C#

private void AssociateAssetWithCampaigns(GoogleAdsClient client, long customerId,
    string promotionAssetResourceName, List<long> campaignIds)
{
    if (campaignIds.Count == 0)
    {
        Console.WriteLine("Asset was not associated with any campaigns.");
        return;
    }

    CampaignAssetServiceClient campaignAssetServiceClient = client.GetService(Services.V8
        .CampaignAssetService);

    List<CampaignAssetOperation> operations = new List<CampaignAssetOperation>();

    foreach (long campaignId in campaignIds)
    {
        operations.Add(new CampaignAssetOperation
        {
            Create = new CampaignAsset
            {
                Asset = promotionAssetResourceName,
                FieldType = AssetFieldTypeEnum.Types.AssetFieldType.Promotion,
                Campaign = ResourceNames.Campaign(customerId, campaignId),
            }
        });
    }

    MutateCampaignAssetsResponse response = campaignAssetServiceClient.MutateCampaignAssets(
        customerId.ToString(), operations);

    foreach (MutateCampaignAssetResult result in response.Results)
    {
        Console.WriteLine($"Created campaign asset with resource name " +
            $"{result.ResourceName}.");
    }
}
      

PHP

private static function associateAssetWithCampaigns(
    GoogleAdsClient $googleAdsClient,
    int $customerId,
    string $promotionAssetResourceName,
    array $campaignIds
) {
    if (empty($campaignIds)) {
        print 'Asset was not associated with any campaigns.' . PHP_EOL;
        return;
    }
    $operations = [];
    foreach ($campaignIds as $campaignId) {
        $operations[] = new CampaignAssetOperation([
            'create' => new CampaignAsset([
                'asset' => $promotionAssetResourceName,
                'field_type' => AssetFieldType::PROMOTION,
                'campaign' => ResourceNames::forCampaign($customerId, $campaignId)
            ])
        ]);
    }
    // Issues a mutate request to add the campaign assets and prints their information.
    $campaignAssetServiceClient = $googleAdsClient->getCampaignAssetServiceClient();
    $response = $campaignAssetServiceClient->mutateCampaignAssets($customerId, $operations);
    foreach ($response->getResults() as $addedCampaignAsset) {
        /** @var CampaignAsset $addedCampaignAsset */
        printf(
            "Created campaign asset with resource name: '%s'.%s",
            $addedCampaignAsset->getResourceName(),
            PHP_EOL
        );
    }
}
      

Python

def _associate_asset_with_campaigns(
    client, customer_id, promotion_asset_resource_name, campaign_ids
):
    """Associates the specified promotion asset with the specified campaigns.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        promotion_asset_resource_name: the resource name for a promotion asset.
        campaign_ids: a list of campaign IDs.
    """
    if len(campaign_ids) == 0:
        print(f"Asset was not associated with any campaigns.")
        return

    campaign_service = client.get_service("CampaignService")
    campaign_asset_service = client.get_service("CampaignAssetService")

    operations = []

    for campaign_id in campaign_ids:
        operation = client.get_type("CampaignAssetOperation")
        campaign_asset = operation.create
        campaign_asset.asset = promotion_asset_resource_name
        campaign_asset.field_type = client.enums.AssetFieldTypeEnum.PROMOTION
        campaign_asset.campaign = campaign_service.campaign_path(
            customer_id, campaign_id
        )
        operations.append(operation)

    response = campaign_asset_service.mutate_campaign_assets(
        customer_id=customer_id, operations=operations
    )

    for result in response.results:
        print(
            "Created campaign asset with resource name: "
            f"'{result.resource_name}'"
        )


      

Ruby

def associate_asset_with_campaigns(client, customer_id, promotion_asset_resource_name, campaign_ids)
  # Associates the specified promotion asset with the specified campaigns.

  if campaign_ids.empty?
    puts 'Asset was not associated with any campaigns.'
    return
  end

  operations = campaign_ids.map do |campaign_id|
    client.operation.create_resource.campaign_asset do |ca|
      ca.asset = promotion_asset_resource_name
      ca.field_type = :PROMOTION
      ca.campaign = client.path.campaign(customer_id, campaign_id)
    end
  end

  response = client.service.campaign_asset.mutate_campaign_assets(
    customer_id: customer_id,
    operations: operations,
  )

  response.results.each do |result|
    puts "Created campaign asset with resource name '#{result.resource_name}'."
  end
end
      

Perl

sub associate_asset_with_campaigns {
  my ($api_client, $customer_id, $promotion_asset_resource_name, @campaign_ids)
    = @_;

  if (scalar(@campaign_ids) == 0) {
    printf "Asset was not associated with any campaigns.\n";
    return ();
  }

  my $operations = [];

  foreach my $campaign_id (@campaign_ids) {
    my $campaign_asset =
      Google::Ads::GoogleAds::V8::Resources::CampaignAsset->new({
        asset     => $promotion_asset_resource_name,
        fieldType => PROMOTION,
        campaign  => Google::Ads::GoogleAds::V8::Utils::ResourceNames::campaign(
          $customer_id, $campaign_id
        )});

    my $operation =
      Google::Ads::GoogleAds::V8::Services::CampaignAssetService::CampaignAssetOperation
      ->new({
        create => $campaign_asset
      });

    push @$operations, $operation;
  }

  my $response = $api_client->CampaignAssetService()->mutate({
    customerId => $customer_id,
    operations => $operations
  });

  foreach my $result (@{$response->{results}}) {
    printf "Created campaign asset with resource name '%s'.\n",
      $result->{resourceName};
  }
}
      

Verify the asset contents

Use the following Google Ads Query Language query to fetch the contents of a promotion asset. Inspect the results to confirm that all relevant fields have been copied correctly.

SELECT
  asset.id,
  asset.name,
  asset.type,
  asset.final_urls,
  asset.final_mobile_urls,
  asset.final_url_suffix,
  asset.tracking_url_template,
  asset.promotion_asset.promotion_target,
  asset.promotion_asset.discount_modifier,
  asset.promotion_asset.redemption_start_date,
  asset.promotion_asset.redemption_end_date,
  asset.promotion_asset.occasion,
  asset.promotion_asset.language_code,
  asset.promotion_asset.percent_off,
  asset.promotion_asset.money_amount_off.amount_micros,
  asset.promotion_asset.money_amount_off.currency_code,
  asset.promotion_asset.promotion_code,
  asset.promotion_asset.orders_over_amount.amount_micros,
  asset.promotion_asset.orders_over_amount.currency_code,
  asset.promotion_asset.start_date,
  asset.promotion_asset.end_date,
  asset.promotion_asset.ad_schedule_targets
FROM asset
WHERE asset.resource_name = 'customers/123456789/assets/123456789012'

Remove the Feed-based extension

Once you have verified that the Asset-based extension accurately reflects the original Feed-based extension, you should remove the Feed-based extension.

Feeds reports availability

Historical feeds reports will continue to be available until August 2022. Feed-based extensions will no longer generate new data after migration.

Asset contents can be requested by issuing a Google Ads Query Language query to the asset report. This report type is akin to the extension_feed_item report type for Feed-based extensions.

An Asset-based extension's performance statistics can be requested by issuing a Google Ads Query Language query to the asset_field_type_view report. This report type is akin to the feed_placeholder_view report type for Feed-based extensions.