Power Pages is my most recent obsession, so I went to try the Client API as soon as I had some time.

Goal is to enhance Bibliotheca with currently reading list, form to add a book and agent to get recommendations and Client API will be used for this.

I know that Sapiens has the Hail Mary cover. Life’s too short for 100% correct mock data. Dune is correct, this is all I care about.

Initialize Client API

With the teeny tiny $pages variable, we get access to forms/lists modifications, Dataverse records, user authentication and more 👀

To start we need Microsoft.Dynamic365.Portal.onPagesClientApiReady function, and it could be used in two ways:

  • Callback-based API readiness

Choose to support older browsers or legacy scripts, event-driven patterns

Microsoft.Dynamic365.Portal.onPagesClientApiReady(($pages) => { ... });
  • Promise/await-based API readiness

Choose for modern environments or if you are already using async/await in the solution

let $pages = await Microsoft.Dynamic365.Portal.onPagesClientApiReady();

Work with Dataverse through Client API

With WebApi in Client API we can:

  • createRecord
  • retrieveRecord
  • retrieveMultipleRecords

To prepare for webapi, in addition to table permissions, we need to add site settings:

  • Webapi/<table name>/enabled
  • Webapi/<table name>/fields

retrieveMultipleRecords method

First method to use - retrieveMultipleRecords:

$pages.webAPI.retrieveMultipleRecords(entitySetName: string, options?: string): Promise<object>

Note the entitySetName: my muscle memory went for logical name and I fought with the API for quite some time before realizing

As I want to show my 3 books in progress, I’ll need:

  • $filter to get only current user’s books with “Reading” status
  • $expand to pull in book details from the related Books table
  • {{ user.id }} via Liquid in a hidden input: <input type="hidden" id="contactId" value="{{ user.id }}" />. Would be nice by the way to have it in $pages.user, in addition to signIn/signOut, just saying.
let $pages = await Microsoft.Dynamic365.Portal.onPagesClientApiReady();
let books = await $pages.webAPI.retrieveMultipleRecords(
'adrbp_userbookstatuses', 
`$select=adrbp_name&
$expand=adrbp_Book($select=adrbp_name,adrbp_author,adrbp_bookimageurl)&
$filter=adrbp_status eq 000000000 and _adrbp_user_value eq ${userId}&
$top=3`);

let container = document.getElementById('books-list');
books.value.forEach((book) => {
    let card = document.createElement('div');
    card.className = 'book-card';
    card.innerHTML = cardInnerHTML(
      book.adrbp_Book.adrbp_bookimageurl, 
      book.adrbp_Book.adrbp_name, 
      book.adrbp_Book.adrbp_author
    );
    container.appendChild(card);
});
function cardInnerHTML(imageUrl, title, author) {  
    return `<img src="${imageUrl}" alt="${title}" class="book-card-image">  
                <div class="book-card-details">  
                <span class="book-card-title">${title}</span>  
                <span class="book-card-author">${author}</span>  
            </div>`  
}

I’m definitely spoiled by years with React and Sass 🌚

But here’s what we got:

createRecord method

Library has to grow, and for this we need “Add book” option. So to create record in the table - createRecord:

$pages.webAPI.createRecord(entitySetName: string, data: object): Promise<object>
// Returns: A Promise that resolves to the created record or operation result.

This is a different table, don’t forget to add webapi site settings

Simplified form layout:

<form id="add-book-form" class="addbook-form" novalidate>
    <div class="form-group">
        <label for="book-title" class="form-label">
          Title 
          <span class="required">*</span>
        </label>
        <input type="text" id="book-title" class="form-input" required />
    </div>
    <!-- Author (required), Genre, Year, ISBN, Cover URL, Description -->
    <button type="submit" id="add-book-submit" class="btn-submit">Add Book</button>
</form>

In actual code we need target table and object we want to create: await $pages.webAPI.createRecord('adrbp_books', record);

let $pages = await Microsoft.Dynamic365.Portal.onPagesClientApiReady();

// --- Form setup ---
const form = document.getElementById('add-book-form');

// --- Genre dropdown (choice column values) ---
const genreSelect = document.getElementById('book-genre');
const GENRE_OPTIONS = [
    { value: 455810000, label: 'Fiction' },
    // ... other genres
];
genreSelect.innerHTML = '<option value="">Select a genre...</option>';
GENRE_OPTIONS.forEach(({ value, label }) => {
    const el = document.createElement('option');
    el.value = value;
    el.textContent = label;
    genreSelect.appendChild(el);
});

// --- Submit handler ---
form.addEventListener('submit', async (e) => {
    e.preventDefault();
    const title = document.getElementById('book-title').value.trim();
    const author = document.getElementById('book-author').value.trim();
    if (!title || !author) {
        showMessage('Title and Author are required.', 'error');
        return;
    }

    const record = {
        adrbp_name: title,
        adrbp_author: author,
    };
    const genre = document.getElementById('book-genre').value.trim();
    if (genre) record.adrbp_genre = parseInt(genre);

    try {
        await $pages.webAPI.createRecord('adrbp_books', record);
        showMessage('Book added to the library!', 'success');
        form.reset();
    } catch (err) {
        showMessage('Failed to add book. Please try again.', 'error');
    }
});

So finally, library will get new books added 💫

Ask the Librarian

Another Client API player: $pages.agent. With it we can call our special Librarian agent for book recommendations. This one is a really simple anonymous agent with public access. This baby agent will just suggest a few books, it has no idea about Dataverse tables content. A real agent would have Microsoft authentication and a more complex flow, but $pages.agent doesn’t care about the complexity on the Copilot side, it only needs the agent schema name.

But first, agent should be configured on website (after some struggle with permissions and licenses in order to publish agent at all, maybe this link will save you some time).

In Power Pages -> Set up -> Agents -> Add your agent

To send something to the agent we need:

$pages.agent.SendActivity(
    agentSchemaName: string,
    inputActivity: object,
    responseSubscriber: function,
    errorSubscriber: function
);

Here we trigger an agent and get response:

let $pages = await Microsoft.Dynamic365.Portal.onPagesClientApiReady();

const magicBtn = document.getElementById('magic-button');
const agentResponse = document.getElementById('agent-response');

magicBtn.addEventListener('click', () => {
    agentResponse.textContent = 'Finding your next read...';

    const inputActivity = { 
        text: 'recommend 3 books, return only the titles separated by commas' 
    };
    const responseSubscriber = (activity) => {
        if (activity.text) {
            const books = activity.text.split(',').map(b => b.trim()).filter(Boolean);
            agentResponse.innerHTML = books.map(b => `<p>${b}</p>`).join('');
        }
    };
    const errorSubscriber = (error) => {
        agentResponse.textContent = 'Something went wrong. Please try again.';
        console.error('Agent error:', error);
    };
    $pages.agent.SendActivity(
        'bibliotheca_agent', 
        inputActivity, 
        responseSubscriber, 
        errorSubscriber
    );
});

Now ✨magic button✨ can do some magic, trigger agent and get list of books

Wrapping Up

Waiting for release (fingers crossed, GA in July 2026) and more methods for each operation (I need my D and U, full CRUD). Anyway, love this update, not flashy, but smth that makes development more focused on the final goal.

References