Bulk property update example extension

So I’m going to quickly run through a AEM content fragment console extension and this is an example extension that performs a bulk property update on content fragments. So first I’ll show you the extension in action. I’ll run it locally and mounted onto my aim as a cloud service environment and update some properties and then we’ll go back after we’ve seen that and take a look at the code that backs the extension. So first I’m going to just run AIO app Run and this is going to run it locally and this is going to allow me to mount the local version of the extension on my aim as a cloud service AEM content environment console using the specially crafted URL which is described in the documentation. So I have that here. Let me go ahead and just refresh.
And now when I pick one or more content fragments from the list, you can see that I have this bulk property update button and this is the action bar button for my custom extension. So I’m going to select a person content fragment and then I’ll select a couple adventure content fragments and I’ll explain why I’m picking a mix of these when I, as I run it. So I’ve selected the content fragments whose property I want to bulk update, click on the custom button, and this opens up my custom modal. So modals, of course, are optional if you don’t need a modal for your extension you certainly don’t have to implement one. But in this case I need to collect the property name as well as the value I want to update my content fragments with. So I’m going to update the group size, I’ll type in group size. This is going to be the content fragment property name that I want to update and then I can put a value in. So I’ll just set it to 999 just to make it very apparent that we’re updating it using this extension here. So one thing to note is with this particular extension’s implementation, it’s going to try to update every single content fragment property called group size with this value. And the adventure content fragment model actually has a property called group size, but the person does not. So what we should see is we should see a bunch of successes for all of our adventure content fragments and we should see a response with one error telling us that the Jacob Webster person content fragment could not be updated because it doesn’t have the group size attached to its model. All right, so let’s go ahead and try this out.
And we get a list backed in our modal and it tells us what was successfully updated. So right, we have group size was updated 99 we have five that were successfully updated and here they are. And these of course are all adventures. And then we have one just like we expect that was unable to update and that was the person because it doesn’t have a group size property on the content fragment model for person. So let’s go ahead and close that and I’m just going to click in here and just to make sure we see that this is in fact updated. And as you can see here, our group size was indeed set to 99 through the bulk update extension.
Okay, so I’m going to quickly just run through the code behind this and you can read the documentation for more details. But this is an action bar content fragment extension. So this was generated with the AIO app in it command. I selected the content fragment extension template and I selected to include a modal as well as a AIO runtime action. So let’s first start with the extensions registration. So we’ll actually go to the app JS first and you can see that I have a route in my extension application that is the index route. So this is the default route and it’s pointing to the registration react component. And so I can open that up since this is the default route here that’s going to get executed and we can see that the very top I have a quick check to make sure that this is only loaded on my specific program that I want to load this on. So I actually have multiple programs within my org but I don’t want to pollute those with this particular extension. So I limit this extension to only working on AEM as a cloud service content fragment consoles that are part of this program right here. And if it isn’t, it’s going to just immediately return and not inject the extension point. Next up here, this is, this should look pretty familiar. This is just the registration for an action bar. So I have the action bar key, I define the button. So I provided a unique ID. I provided the label for the button, right? So this is exactly what shows up over here in the top action bar. And then I specify the icon as well.
Next up is the on click handler. And this takes in a list of the selections. So these selections variable essentially represents all of the content fragments that I have selected here. So we serialize the list of selections into paths. So selection ID is the content fragment path and then we construct a URL. So the modal URL that is going to map to a route in our extension react app. And I’ll jump back to app.js in a sec just to show you that. We’re able to populate the selection parameter in this URL. So essentially this is defining what’s going to go into here in the URL and we’ve encoded all of these selected content fragment paths and those are going to get passed into our application in that manner. And the last thing we’re going to do here is we’re going to actually open up and show the URL whenever the button is clicked. So we can call the guest connection host modal show URL, give the modal a title, and this is actually what shows up if I click it again, this is what shows up here in the modal and it opens up the modal using whatever this URL is which in this case maps to this react route with the selection parameter set to the content fragment paths. And just to kind of tie that up a little bit, let’s go back to apps. All right, and you can see that we have a route, it maps to that modal URL we just looked at with the selection parameter. And it will display the bulk property update modal react component. So essentially when you click on that button it’s going to open up the modal and then it’s going to load whatever’s rendered in this inside of the modal itself. So we can open up this modal react component.
This should look pretty familiar to you as a functional react component. It does use the react spectrum library which is a UI or UX library provided by Adobe. And that’s what gets us the same styling as the rest of AEM. So right, so the blue buttons or the buttons with the outlines and the fields and everything like this. So it’s a pretty rich UX UI framework so definitely encourage you to take advantage of that.
So going down in our modal, we set up a little bit of state. So first of all, we need to make sure we get the guest connection. This is kind of what links the modal to AEM itself. So we can get that, we’ll see down here in a moment.
Then we set up some state to manage the invocation and response of the Adobe I/O runtime action which is what we’ll be using to actually interact with AIM as a cloud service and set those properties to the specified values. And then, and down here we have some state that manages the actual form of the modal. So for instance, the property name field gets mapped to this property name, state property values here and then we have a little validation state as well.
Next up we grab the selection parameter that’s passed into us. And again, that is, if we look here, that’s going to be passed in right here using this parameter name.
So we basically can grab that value and since we delimited it with the pipe we just split it on the pipe. And this is going to give us a list of the content fragment IDs or paths.
So we’ll just do a little checking on that. Next up we have a use effect. So this is asynchronous. So we’re just using this to get the guest connection and attach the extension to the greater context here.
And then at the end here we just have an IFELFs block that’s going to determine what should be displayed in the modal, right? So if the guest connection hasn’t been initialized yet we’re going to show a spinner. If the action is in progress, right? So if we had selected, you know 30 content fragments and it’s going to take, you know, 30 seconds to update them all during that process, we’re going to show a spinner as well. If we get a response back from our Adobe I/O run time action then we’re going to show the response and otherwise we’re just going to show this form over here. So here’s the form.
So to render the form, again we’re using the Adobe React spectrum library. So you’ll see a lot of stuff like provider content, flex form, these are all react spectrum components. So it’s again, it’s a pretty rich UX kit and you can build out all sorts of great UIs and experiences with it. This one’s pretty simple, so we just have a form, right? We have a text field that manages the state for the property name. Same thing for property value. We have a call to action button that’s going to invoke our submit handler. So that’s our blue button down here. And then we also need to render a close button ourselves. So if we wanted to close this modal this is the close button and that’s something we provide then the modal itself.
So after these values are filled out so let me go ahead and do that. We have our on submit handler. So we’re going to on clicking our call to action blue button down here, it’s going to invoke our method here. Does a little bit of validation just to make sure that the form is filled out properly. And let me just hide this so we can see it a little bit better. And then what we do is we set the invoke action in progress state to true. And so this is going to start showing our spinner. We set some authorization headers that we need to use to contact the Adobe I/O runtime action that we’ve implemented. So we set an authorization header as well as this XGW IMS org ID and we can get the values from the guest connection. And again, this is kind of why we need to make sure we wait for the guest connection to initialize and attach before we can start doing anything with our extension. Next up, we set up some of the parameters that we’re going to pass to our Adobe I/O runtime action. So one thing we’re going to send over is the AEM host. So this is the, you know, the AEM is a cloud service environment that this content fragment console is attached to. And so this is what we’re going to want to connect to when we want to do our updates. So we can get that value here and we can simply pass into the list of content fragment IDs as well as the property name and value that we want to update for all of them. The next block here is invoking our Adobe I/O run time action. And this is again, this is the action that’s actually going to reach out to AEM as a cloud service and update those content fragments. So we’re just using the generic action name. You can obviously change this name or if you had multiple actions you could select the one you want. But we can essentially get the URL to the action that we want to invoke through the all actions that we’re importing. And I can show you this as well. All actions is really, go up here to the top. It’s a little bit interesting. So all actions is really just import of the config.jason. And so if we open that up we can see that this is just an object and it’s really just a key value pair of the action name and a URL to the actual action that we want to invoke.
So essentially, oops.
Sorry about that. In our modal here.
So essentially what we’re doing here is we’re just passing in the URL to the Adobe I/O runtime action that we want, passing in some headers as well as any perams. And since this is asynchronous, we want to wait for it to get done and then get the response. We can set the response to the react state so we can display it later. And then finally, we can set the action invoke progress to false which is going to get rid of the progress spinner for us.
So let’s jump into the actual Adobe I/O runtime action that we’re invoking using this action web invoke call. So if you remember earlier I said that we created this action using AIO app and we had selected a modal as well as a Adobe I/O runtime action. So when we selected the use action, it created this generic action here for us.
So opening this up here is the Adobe I/O runtime action that’s actually going to communicate with AEM and update those properties. So again, let me do this.
This is pretty standard for a runtime action. We have some required parameters that we can validate, some required headers. So this just is a little bit of validation to make sure that it’s a well form request. Here’s going to be the body of the http put call to AEM as a cloud service to update those content fragments. So we’re essentially formulating this to say update the parameterized property name with the parameterized value. Next up we get the Bear token. This is a method provided by some of the AIO runtime utils and this is going to return the bearer token for the user that invoked this action. So essentially like the logged in user that clicked this button, this bearer token will belong to that user. So our action can act as if it’s the user that invoked the modal.
Next up here is the meat of the action. And this is just going to cycle through the list of content fragments that are passed in, right? So we have a, for each fragment ID and we’ll make a fetch call to AEM and we’re going to use the AEM assets HDP API to update the content fragment property using the put method. And you’ll see again that we’re using the authorization header here with the bearer token of the access token which is again extracted up here.
So, we’ll actually be interacting with AEM with the same credentials as the user that clicked the button here, even though this is a serverless asynchronous action in the background. Once we’ve done this, we’re going to wait for all them to get done with the await promise all and then we’re just going to kind of collate the success and failures into a response and send that response back to the caller, which is our modal. All right, so we’ll send that back to this and that’s what’s going to get stored in the our action response.
So now we’ll go back up to our if false clause here. So at this point we have an action response. So now we’re going to render the response. And again, should look actually pretty similar to the form. This is going to be using the react spectrum framework for the display. It’s going to essentially pull out all the successes from that action response. So it’s going to go through all of the items in it. Anything with a 200 item is going to say it’s a success and anything not 200, it’ll be a failure. And then it just essentially goes through here and list them out into list views of successes at the top and with failures down below. So again, we can see that. I can click here and you can see I have my list of successes and any failures below that.
So that is about it for this example, bulk property update, action bar content fragment extension. And hope this helps you understand a little bit more how it was built and how you might build one of your own. -

This example AEM Content Fragment Console extension is an action bar extension that bulk updates a Content Fragment property to a common value.

The functional flow of the example extension is as follows:

Adobe I/O Runtime action flow {align="center"}

  1. Select Content Fragments and clicking the extension’s button in the action bar opens the modal.
  2. The modal displays a custom input form built with React Spectrum.
  3. Submitting the form sends the list of selected Content Fragments, and the AEM host to the custom Adobe I/O Runtime action.
  4. The Adobe I/O Runtime action validates the inputs and makes HTTP PUT requests to AEM to update the selected Content Fragments.
  5. A series of HTTP PUTs for each Content Fragment to update the specified property.
  6. AEM as a Cloud Service persists the property updates to the Content Fragment and returns success or failure responses to the Adobe I/O Runtime action.
  7. The modal received the response from the Adobe I/O Runtime action, and displays a list of successful bulk updates.

Extension point

This example extends to extension point actionBar to add custom button to the Content Fragment Console.

AEM UI extended
Extension point
Content Fragment Console
Action Bar

Example extension

The example uses an existing Adobe Developer Console project, and uses the following options when initializing the App Builder app, via aio app init.

  • What templates do you want to search for?: All Extension Points

  • Choose the template(s) to install: @adobe/aem-cf-admin-ui-ext-tpl

  • What do you want to name your extension?: Bulk property update

  • Please provide a short description of your extension: An example action bar extension that bulk updates a single property one or more content fragments.

  • What version would you like to start with?: 0.0.1

  • What would you like to do next?

    • Add a custom button to Action Bar

      • Please provide label name for the button: Bulk property update
      • Do you need to show a modal for the button? y
    • Add server-side handler

      • Adobe I/O Runtime lets you invoke serverless code on demand. How would you like to name this action?: generic

The generated App Builder extension app is updated as described below.

App routes app-routes

The src/aem-cf-console-admin-1/web-src/src/components/App.js contains the React router.

There are two logical sets of routes:

  1. The first route maps requests to the index.html, which invokes the React component responsible for the extension registration.

    code language-javascript
    <Route index element={<ExtensionRegistration />} />
  2. The second set of routes maps URLs to React components that render the contents of the extension’s modal. The :selection param represents a delimited list content fragment path.

    If the extension has multiple buttons to invoke discrete actions, each extension registration maps to a route defined here.

    code language-javascript
        exact path="content-fragment/:selection/bulk-property-update"
        element={<BulkPropertyUpdateModal />}

Extension registration

ExtensionRegistration.js, mapped to the index.html route, is the entry point for the AEM extension and defines:

  1. The location of the extension button appears in the AEM authoring experience (actionBar or headerMenu)
  2. The extension button’s definition in getButtons() function
  3. The click handler for the button, in the onClick() function
  • src/aem-cf-console-admin-1/web-src/src/components/ExtensionRegistration.js
import React from "react";
import { generatePath } from "react-router";
import { Text } from "@adobe/react-spectrum";
import { register } from "@adobe/uix-guest";
import { extensionId } from "./Constants";

function ExtensionRegistration() {
  const init = async () => {
    const guestConnection = await register({
      id: extensionId, // Some unique ID for the extension used to facilitate communication between the extension and Content Fragment Console
      methods: {
        // Configure your Action Bar button here
        actionBar: {
          getButtons() {
            return [
                id: "examples.action-bar.bulk-property-update", // Unique ID for the button
                label: "Bulk property update", // Button label
                icon: "OpenIn", // Button icon; get name from: https://spectrum.adobe.com/page/icons/ (Remove spaces, keep uppercase)
                // Click handler for the extension button
                onClick(selections) {
                  // Collect the selected content fragment paths
                  const selectionIds = selections.map(
                    (selection) => selection.id

                  // Create a URL that maps to the
                  const modalURL =
                    "/index.html#" +
                        // Set the :selection React route parameter to an encoded, delimited list of paths of the selected content fragments
                        selection: encodeURIComponent(selectionIds.join("|")),

                  // Open the route in the extension modal using the constructed URL
                    // The modal title
                    title: "Bulk property update",
                    url: modalURL,


  return <Text>IFrame for integration with Host (AEM)...</Text>;

export default ExtensionRegistration;

Each route of the extension, as defined in App.js, maps to a React component that renders in the extension’s modal.

In this example app, there is a modal React component (BulkPropertyUpdateModal.js) that has three states:

  1. Loading, indicating the user must wait
  2. The Bulk Property Update form that allows the user to specify the property name and value to update
  3. The response of the bulk property update operation, listing the content fragments that were updated, and those that could not be updated

Importantly, any interaction with AEM from the extension should be delegated to an AppBuilder Adobe I/O Runtime action, which is a separate serverless process running in Adobe I/O Runtime.
The use of Adobe I/O Runtime actions to communicate with AEM is to avoid Cross-Origin Resource Sharing (CORS) connectivity issues.

When the Bulk Property Update form is submitted, a custom onSubmitHandler() invokes the Adobe I/O Runtime action, passing the current AEM host (domain) and user’s AEM access token, which in turn calls the AEM Content Fragment API to update the content fragments.

When the response from the Adobe I/O Runtime action is received, the modal is updated to display the results of the bulk property update operation.

  • src/aem-cf-console-admin-1/web-src/src/components/BulkPropertyUpdateModal.js
import React, { useState, useEffect } from 'react'
import { attach } from "@adobe/uix-guest"
import {
} from '@adobe/react-spectrum'

import Spinner from "./Spinner"

import { useParams } from "react-router-dom"

import allActions from '../config.json'
import actionWebInvoke from '../utils'

import { extensionId } from "./Constants"

export default function BulkPropertyUpdateModal() {
  // Set up state used by the React component
  const [guestConnection, setGuestConnection] = useState()

  const [actionInvokeInProgress, setActionInvokeInProgress] = useState(false);
  const [actionResponse, setActionResponse] = useState();

  const [propertyName, setPropertyName] = useState(null);
  const [propertyValue, setPropertyValue] = useState(null);
  const [validationState, setValidationState] = useState({});

  // Get the selected content fragment paths from the route parameter `:selection`
  let { selection } = useParams();
  let fragmentIds = selection?.split('|') || [];

  console.log('Content Fragment Ids', fragmentIds);

  if (!fragmentIds || fragmentIds.length === 0) {
    console.error("Unable to locate a list of Content Fragments to update.")

  // Asynchronously attach the extension to AEM, we must wait or the guestConnection to be set before doing anything in the modal
  useEffect(() => {
    (async () => {
       // extensionId is the unique id of this extension (you can make this up as long as its unique) .. in this case its `bulk-property-update` pulled out into Constants.js as it is also referenced in ExtensionRegistration.js
      const guestConnection = await attach({ id: extensionId })
  }, [])

  // Determine view to display in the modal
  if (!guestConnection) {
    // If the guestConnection is not initialized, display a loading spinner
    return <Spinner />
  } else if (actionInvokeInProgress) {
    // If the bulk property action has been invoked but not completed, display a loading spinner
    return <Spinner />
  } else if (actionResponse) {
    // If the bulk property action has completed, display the response
    return renderResponse();
  } else {
    // Else display the bulk property update form
    return renderForm();

   * Renders the Bulk Property Update form.
   * This form has two fields:
   * - Property Name: The name of the Content Fragment property name to update
   * - Property Value: the value the Content Fragment property, specified by the Property Name, will be updated to
   * @returns the Bulk Property Update form
  function renderForm() {
    return (
      // Use React Spectrum components to render the form
      <Provider theme={defaultTheme} colorScheme='light'>
        <Content width="100%">
          <Flex width="100%">
              <TextField label="Property name"
                    <Heading>Need help?</Heading>
                      <Text>The <strong>Property name</strong> must be a valid for the Content Fragment model(s) the selected Content Fragments implement.</Text>
                } />

                label="Property value"
                onChange={setPropertyValue} />

              <ButtonGroup align="start" marginTop="size-200">
                <Button variant="cta" onPress={onSubmitHandler}>Update {fragmentIds.length} Content Fragments</Button>

          {/* Render the close button so the user can close the modal */}
   * Display the response from the Adobe I/O Runtime action in the modal.
   * This includes:
   * - A list of content fragments that were updated successfully
   * - A list a content fragments that failed to update
   * @returns the response view
  function renderResponse() {
    // Separate the successful and failed content fragments updates
    const successes = actionResponse.filter(item => item.status === 200);
    const failures = actionResponse.filter(item => item.status !== 200);

    return (
      <Provider theme={defaultTheme} colorScheme='light'>
        <Content width="100%">

          <Text>Bulk updated property <strong>{propertyName}</strong> with value <strong>{propertyValue}</strong></Text>

          {/* Render the list of content fragments that were updated successfully */}
          {successes.length > 0 &&
            <><Heading level="4">{successes.length} Content Fragments successfully updated</Heading>
                aria-label="Successful updates"
                {(item) => (
                  <Item key={item.fragmentId} textValue={item.fragmentId.split('/').pop()}>

          {/* Render the list of content fragments that failed to update */}
          {failures.length > 0 &&
            <><Heading level="4">{failures.length} Content Fragments failed to update</Heading><ListView
              aria-label="Failed updates"
              {(item) => (
                <Item key={item.fragmentId} textValue={item.fragmentId.split('/').pop()}>

          {/* Render the close button so the user can close the modal */}

   * Provide a close button for the modal, else it cannot be closed (without refreshing the browser window)
   * @returns a button that closes the modal.
   function renderCloseButton() {
    return (
      <Flex width="100%" justifyContent="end" alignItems="center" marginTop="size-400">
        <ButtonGroup align="end">
          <Button variant="primary" onPress={() => guestConnection.host.modal.close()}>Close</Button>

   * Handle the Bulk Property Update form submission.
   * This function calls the supporting Adobe I/O Runtime action to update the selected Content Fragments, and then returns the response for display in the modal
   * When invoking the Adobe I/O Runtime action, the following parameters are passed as they're used by the action to connect to AEM:
   * - AEM Host to connect to
   * - AEM access token to connect to AEM with
   * - The list of Content Fragment paths to update
   * - The Content Fragment property name to update
   * - The value to update the Content Fragment property to
   * @returns a list of content fragment update successes and failures
  async function onSubmitHandler() {
    // Validate the form input fields
    if (propertyName?.length > 1) {
      setValidationState({propertyName: 'valid', propertyValue: 'valid'});
    } else {
      setValidationState({propertyName: 'invalid', propertyValue: 'valid'});

    // Mark the extension as invoking the action, so the loading spinner is displayed

    // Set the HTTP headers to access the Adobe I/O runtime action
    const headers = {
      'Authorization': 'Bearer ' + guestConnection.sharedContext.get('auth').imsToken,
      'x-gw-ims-org-id': guestConnection.sharedContext.get('auth').imsOrg

    console.log('headers', headers);

    // Set the parameters to pass to the Adobe I/O Runtime action
    const params = {
      aemHost: `https://${guestConnection.sharedContext.get('aemHost')}`,

      fragmentIds: fragmentIds,
      propertyName: propertyName,
      propertyValue: propertyValue

    // Invoke the Adobe I/O Runtime action named `generic`. This name defined in the `ext.config.yaml` file.
    const action = 'generic';

    try {
      // Invoke Adobe I/O Runtime action with the configured headers and parameters
      const actionResponse = await actionWebInvoke(allActions[action], headers, params);

      // Set the response from the Adobe I/O Runtime action

      console.log(`Response from ${action}:`, actionResponse)
    } catch (e) {
      // Log and store any errors

    // Set the action as no longer being invoked, so the loading spinner is hidden

Adobe I/O Runtime action

An AEM extension App Builder app can define or use 0 or many Adobe I/O Runtime actions.
Adobe Runtime actions should be responsible work that requires interacting with AEM, or other Adobe web services.

In this example app, the Adobe I/O Runtime action - which uses the default name generic - is responsible for:

  1. Making a series of HTTP requests to the AEM Content Fragment API to update the content fragments.
  2. Collecting the responses of these HTTP requests, collating them into successes and failures
  3. Returning the list of successes and failure for display by the modal (BulkPropertyUpdateModal.js)
  • src/aem-cf-console-admin-1/actions/generic/index.js

const fetch = require('node-fetch')
const { Core } = require('@adobe/aio-sdk')
const { errorResponse, getBearerToken, stringParameters, checkMissingRequestInputs } = require('../utils')

// main function that will be executed by Adobe I/O Runtime
async function main (params) {
  // create a Logger
  const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' })

  try {
    // 'info' is the default level if not set
    logger.info('Calling the main action')

    // log parameters, only if params.LOG_LEVEL === 'debug'

    // check for missing request input parameters and headers
    const requiredParams = [ 'aemHost', 'fragmentIds', 'propertyName', 'propertyValue' ]
    const requiredHeaders = ['Authorization']
    const errorMessage = checkMissingRequestInputs(params, requiredParams, requiredHeaders)
    if (errorMessage) {
      // return and log client errors
      return errorResponse(400, errorMessage, logger)

    const body = {
      "properties": {
        "elements": {
          [params.propertyName]: {
            "value": params.propertyValue

    // Extract the user Bearer token from the Authorization header used to authenticate the request to AEM
    const accessToken = getBearerToken(params);

    let results = await Promise.all(params.fragmentIds.map(async (fragmentId) => {

      logger.info(`Updating fragment ${fragmentId} with property ${params.propertyName} and value ${params.propertyValue}`);

      const res = await fetch(`${params.aemHost}${fragmentId.replace('/content/dam/', '/api/assets/')}.json`, {
        method: 'put',
        body: JSON.stringify(body),
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'

      if (res.ok) {
        logger.info(`Successfully updated ${fragmentId}`);
        return { fragmentId, status: res.status, statusText: res.statusText, body: await res.json() };
      } else {
        logger.info(`Failed to update ${fragmentId}`);
        return { fragmentId, status: res.status, statusText: res.statusText, body: await res.text() };

    const response = {
      statusCode: 200,
      body: results

    logger.info('Adobe I/O Runtime action response', response);

    // Return the response to the A
     return response;

  } catch (error) {
    // log any server errors
    // return with 500
    return errorResponse(500, 'server error', logger)

exports.main = main