Airgentic Help
This guide covers the additional steps required when hosting the Airgentic secure widget on a SharePoint Online intranet. It builds on the general Adding the Secure Widget to Your Site guide — complete those steps first, then follow the SharePoint-specific guidance below.
SharePoint Online has security restrictions that affect how external scripts are loaded and executed. The key things to be aware of are:
<script> tags. You cannot simply paste the Airgentic embed code into a page editor.The Airgentic SPFx web part acts as a placement anchor for the inline Service Hub. When you add the web part to a SharePoint page:
This design means customers control exactly where the Service Hub appears by placing the web part in the desired location on each page.
SharePoint Online's Content Security Policy blocks external scripts that are not explicitly trusted. Your SharePoint administrator must add the Airgentic script domain as a trusted source.
https://<your-tenant>-admin.sharepoint.com.https://chat.airgentic.com
Note: If you cannot find this setting in the admin center UI, your SharePoint administrator may need to configure it via PowerShell or the SharePoint API. Microsoft's documentation on allowing custom scripts provides additional guidance.
Without this step, the Airgentic widget script will be blocked by the browser and the widget will not load.
On modern SharePoint pages, you cannot insert a <script> tag directly. Instead, you need a SharePoint Framework (SPFx) web part that loads the Airgentic script.
Airgentic provides a ready-to-deploy SPFx package. No build step required — just download, upload to your App Catalog, and configure.
Download airgentic-webpart.sppkg
To deploy:
.sppkg file using the link above.https://<your-tenant>.sharepoint.com/sites/appcatalog)..sppkg file.Airgentic will provide your Account ID and Service ID.
The same .sppkg includes an Application Customizer that can load the Airgentic hover widget on every modern SharePoint page without adding the web part to each page. It is off until your administrator enables it tenant-wide (see below).
How it works
#airgentic-script) with data-mode="launcher" for the global launcher.1. Set tenant properties (one-time, tenant admin)
Install PnP PowerShell if needed, then run (replace placeholders):
Connect-PnPOnline -Url "https://yourorg-admin.sharepoint.com" -Interactive
Set-PnPStorageEntity -Key "AirgenticAccountId" -Value "your_account_id"
Set-PnPStorageEntity -Key "AirgenticServiceId" -Value "your_service_id"
Set-PnPStorageEntity -Key "AirgenticRedirectUri" -Value "https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx"
Use the same Account ID, Service ID, and Redirect URI values you use for the web part (typically the callback page URL for Redirect URI).
Microsoft’s reference: SharePoint Framework tenant properties.
2. Deploy the package and enable the extension tenant-wide
After uploading airgentic-webpart.sppkg to the App Catalog, register the Application Customizer for the tenant. The component ID is:
c1d2e3f4-a5b6-4789-a012-3456789abcde
Options include:
m365 spo tenant applicationcustomizer add --title "Airgentic global launcher" --clientSideComponentId c1d2e3f4-a5b6-4789-a012-3456789abcdeIt can take several minutes for the extension to apply after the first registration.
Inline Service Hub vs global launcher
| Capability | Web part | Application Customizer |
|---|---|---|
| Inline Service Hub in a chosen section/column | Yes | No |
| Hover / launcher on many pages without per-page setup | No (unless you add web parts everywhere) | Yes (when enabled) |
| Configuration | Property pane on each instance | Tenant storage entities (shared) |
If you want to use Airgentic's Search UI (a unified site search + chat overlay), the web part also supports these optional settings:
| Setting | Description |
|---|---|
| Search Input ID | The ID of your site's search input element (no # prefix) |
| Search Button ID | The ID of your site's search button element (no # prefix) |
You can provide one or both. When configured, Airgentic will bind to your existing search elements and activate the Search UI overlay when users interact with them.
If your organisation has a SharePoint developer and prefers to build or customise the web part, you can create your own SPFx project. This gives you full control over the web part's behaviour, allows you to integrate it into your existing SharePoint development workflow, and enables customisation such as styling or additional features.
yo @microsoft/sharepoint
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import {
type IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
export interface AirgenticMountConfig {
target: string | HTMLElement;
mode: 'service-hub' | 'inline';
accountId: string;
serviceId: string;
authMode: string;
redirectUri: string;
searchInputId?: string;
searchButtonId?: string;
}
export interface AirgenticMountHandle {
unmount: () => void;
}
declare global {
interface Window {
Airgentic?: {
mount?: (config: AirgenticMountConfig) => AirgenticMountHandle | void;
unmount?: (target: string | HTMLElement) => void;
isReady?: boolean;
};
}
}
export interface IAirgenticWebPartProps {
accountId: string;
serviceId: string;
redirectUri: string;
searchInputId: string;
searchButtonId: string;
}
const RUNTIME_POLL_INTERVAL_MS = 50;
const RUNTIME_WAIT_TIMEOUT_MS = 60000;
export default class AirgenticWebPart extends BaseClientSideWebPart<IAirgenticWebPartProps> {
private static readonly SCRIPT_ID = 'airgentic-script';
private static readonly SCRIPT_SRC = 'https://chat.airgentic.com/airgentic-1.4.js';
private static sharedScriptElementPromise: Promise<void> | undefined;
private mountId: string = '';
private mountHandle: AirgenticMountHandle | null = null;
private thisInstanceInjectedScript: boolean = false;
private scriptError: boolean = false;
private lastInitConfigSignature: string = '';
private renderGeneration: number = 0;
public render(): void {
this.mountId = this.getMountId();
if (!this.isConfigurationValid()) {
this.renderConfigState(this.getMissingConfigFields());
return;
}
const sig = this.getConfigSignature();
if (sig !== this.lastInitConfigSignature) {
this.lastInitConfigSignature = sig;
this.scriptError = false;
this.unmountInlineServiceHub();
}
if (this.scriptError) {
this.renderErrorState();
return;
}
this.renderMountContainer();
const gen = ++this.renderGeneration;
void this.ensureScriptLoaded()
.then(() => {
if (gen !== this.renderGeneration) return;
return this.mountInlineServiceHub();
})
.catch((err: unknown) => {
if (gen !== this.renderGeneration) return;
this.scriptError = true;
const message = err instanceof Error ? err.message : undefined;
this.renderErrorState(message);
});
}
private getMountId(): string {
return `airgentic-${this.instanceId}`;
}
private getConfigSignature(): string {
const p = this.properties;
return [p.accountId, p.serviceId, p.redirectUri, p.searchInputId, p.searchButtonId].join('\u0001');
}
private isConfigurationValid(): boolean {
return this.getMissingConfigFields().length === 0;
}
private getMissingConfigFields(): string[] {
const { accountId, serviceId, redirectUri } = this.properties;
const missing: string[] = [];
if (!accountId?.trim()) missing.push('Account ID');
if (!serviceId?.trim()) missing.push('Service ID');
if (!redirectUri?.trim()) missing.push('Redirect URI');
return missing;
}
private renderConfigState(missingFields: string[]): void {
const items = missingFields.map((f) => `<li>${this.escapeHtml(f)}</li>`).join('');
this.domElement.innerHTML = `
<div style="padding: 20px; border: 1px solid #c7c7c7; border-radius: 4px; background: #f9f9f9;">
<p style="margin: 0 0 10px 0; font-weight: 600;">Airgentic Service Hub — configuration required</p>
<p style="margin: 0; color: #666;">Please configure the web part properties:</p>
<ul style="margin: 10px 0 0 0; color: #666;">${items}</ul>
<p style="margin: 10px 0 0 0; color: #666;">Click the edit (pencil) icon on this web part to open the property pane.</p>
</div>
`;
}
private renderErrorState(message?: string): void {
const extra = message ? `<p style="margin: 10px 0 0 0; color: #666;">${this.escapeHtml(message)}</p>` : '';
this.domElement.innerHTML = `
<div style="padding: 20px; border: 1px solid #d93025; border-radius: 4px; background: #fce8e6;">
<p style="margin: 0; color: #d93025; font-weight: 600;">Failed to load Airgentic</p>
<p style="margin: 10px 0 0 0; color: #666;">Please check that chat.airgentic.com is added to your SharePoint trusted script sources.</p>
${extra}
</div>
`;
}
private renderMountContainer(): void {
this.domElement.innerHTML = `
<div id="${this.escapeHtml(this.mountId)}" class="airgentic-inline-root">
<div class="airgentic-loading" style="padding: 20px; text-align: center; color: #666;">
Loading Airgentic Service Hub...
</div>
</div>
`;
}
private escapeHtml(text: string): string {
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
private ensureScriptLoaded(): Promise<void> {
if (this.isAirgenticRuntimeReady()) return Promise.resolve();
if (!AirgenticWebPart.sharedScriptElementPromise) {
const existing = document.getElementById(AirgenticWebPart.SCRIPT_ID) as HTMLScriptElement | null;
if (existing) {
AirgenticWebPart.sharedScriptElementPromise = AirgenticWebPart.waitForScriptElementReady(existing);
} else {
AirgenticWebPart.sharedScriptElementPromise = new Promise<void>((resolve, reject) => {
this.injectScript(resolve, reject);
});
}
}
return AirgenticWebPart.sharedScriptElementPromise
.then(() => this.waitForAirgenticRuntime())
.catch((err: unknown) => {
AirgenticWebPart.sharedScriptElementPromise = undefined;
throw err;
});
}
private isAirgenticRuntimeReady(): boolean {
return typeof window.Airgentic !== 'undefined';
}
private waitForAirgenticRuntime(): Promise<void> {
if (this.isAirgenticRuntimeReady()) return Promise.resolve();
return new Promise((resolve, reject) => {
const started = Date.now();
const id = window.setInterval(() => {
if (this.isAirgenticRuntimeReady()) {
window.clearInterval(id);
resolve();
} else if (Date.now() - started > RUNTIME_WAIT_TIMEOUT_MS) {
window.clearInterval(id);
reject(new Error('Timed out waiting for Airgentic runtime.'));
}
}, RUNTIME_POLL_INTERVAL_MS);
});
}
private static waitForScriptElementReady(script: HTMLScriptElement): Promise<void> {
return new Promise((resolve, reject) => {
if (script.dataset.airgenticSpfxLoaded === 'true') {
resolve();
return;
}
let settled = false;
let pollId: number = 0;
let timeoutId: number = 0;
const settleOk = (): void => {
if (settled) return;
settled = true;
window.clearInterval(pollId);
window.clearTimeout(timeoutId);
script.dataset.airgenticSpfxLoaded = 'true';
resolve();
};
const settleErr = (): void => {
if (settled) return;
settled = true;
window.clearInterval(pollId);
window.clearTimeout(timeoutId);
reject(new Error('Airgentic script failed to load.'));
};
script.addEventListener('load', settleOk, { once: true });
script.addEventListener('error', settleErr, { once: true });
pollId = window.setInterval(() => {
if (typeof window.Airgentic !== 'undefined') settleOk();
}, RUNTIME_POLL_INTERVAL_MS);
timeoutId = window.setTimeout(() => {
if (!settled && typeof window.Airgentic === 'undefined') settleErr();
}, RUNTIME_WAIT_TIMEOUT_MS);
});
}
private injectScript(resolve: () => void, reject: (reason?: Error) => void): void {
const { accountId, serviceId, redirectUri, searchInputId, searchButtonId } = this.properties;
const script = document.createElement('script');
script.id = AirgenticWebPart.SCRIPT_ID;
script.src = AirgenticWebPart.SCRIPT_SRC;
script.setAttribute('data-account-id', accountId.trim());
script.setAttribute('data-service-id', serviceId.trim());
script.setAttribute('data-auth-mode', 'oidc');
script.setAttribute('data-auth-redirect-uri', redirectUri.trim());
script.setAttribute('data-target-id', this.mountId);
script.setAttribute('data-mode', 'inline');
if (searchInputId?.trim()) script.setAttribute('data-search-input-id', searchInputId.trim());
if (searchButtonId?.trim()) script.setAttribute('data-search-button-id', searchButtonId.trim());
this.thisInstanceInjectedScript = true;
script.onload = () => {
script.dataset.airgenticSpfxLoaded = 'true';
resolve();
};
script.onerror = () => {
if (script.parentNode) script.parentNode.removeChild(script);
this.thisInstanceInjectedScript = false;
reject(new Error('Airgentic script failed to load.'));
};
document.body.appendChild(script);
}
private mountInlineServiceHub(): Promise<void> {
const root = document.getElementById(this.mountId);
if (!root) {
this.scriptError = true;
this.renderErrorState('Mount container was not found.');
return Promise.resolve();
}
const mountFn = window.Airgentic?.mount;
if (typeof mountFn === 'function') {
try {
const result = mountFn.call(window.Airgentic, this.buildMountConfig(root));
if (result && typeof result.unmount === 'function') this.mountHandle = result;
} catch (e) {
this.scriptError = true;
const msg = e instanceof Error ? e.message : 'Mount failed.';
this.renderErrorState(msg);
return Promise.resolve();
}
this.clearLoadingPlaceholder(root);
return Promise.resolve();
}
// Legacy fallback: only the injecting instance can use data-target-id on script tag
if (this.thisInstanceInjectedScript) {
this.clearLoadingPlaceholder(root);
return Promise.resolve();
}
this.scriptError = true;
this.renderErrorState(
'Another Airgentic instance already loaded the script. ' +
'Update the Airgentic runtime to support window.Airgentic.mount() for multiple inline web parts.'
);
return Promise.resolve();
}
private buildMountConfig(target: HTMLElement): AirgenticMountConfig {
const p = this.properties;
const config: AirgenticMountConfig = {
target,
mode: 'service-hub',
accountId: p.accountId.trim(),
serviceId: p.serviceId.trim(),
authMode: 'oidc',
redirectUri: p.redirectUri.trim()
};
if (p.searchInputId?.trim()) config.searchInputId = p.searchInputId.trim();
if (p.searchButtonId?.trim()) config.searchButtonId = p.searchButtonId.trim();
return config;
}
private clearLoadingPlaceholder(root: HTMLElement): void {
const loading = root.querySelector('.airgentic-loading');
if (loading?.parentNode) loading.parentNode.removeChild(loading);
}
private unmountInlineServiceHub(): void {
if (this.mountHandle) {
try { this.mountHandle.unmount(); } catch { /* ignore */ }
this.mountHandle = null;
} else {
const unmount = window.Airgentic?.unmount;
const root = this.mountId ? document.getElementById(this.mountId) : null;
if (typeof unmount === 'function' && root) {
try { unmount.call(window.Airgentic, root); } catch { /* ignore */ }
}
}
}
protected onDispose(): void {
this.unmountInlineServiceHub();
if (this.domElement) this.domElement.innerHTML = '';
// Do NOT remove global script — other instances or future global loader may depend on it
super.onDispose();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [{
groups: [
{
groupName: 'Airgentic Settings',
groupFields: [
PropertyPaneTextField('accountId', {
label: 'Account ID',
description: 'Your Airgentic account ID (provided by Airgentic)'
}),
PropertyPaneTextField('serviceId', {
label: 'Service ID',
description: 'Your Airgentic service ID (provided by Airgentic)'
}),
PropertyPaneTextField('redirectUri', {
label: 'Redirect URI',
description: 'The callback page URL (e.g. https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx)'
})
]
},
{
groupName: 'Search UI (Optional)',
groupFields: [
PropertyPaneTextField('searchInputId', {
label: 'Search Input ID',
description: 'The ID of your site\'s search input element (no # prefix). Leave blank if not using Search UI.'
}),
PropertyPaneTextField('searchButtonId', {
label: 'Search Button ID',
description: 'The ID of your site\'s search button element (no # prefix). Leave blank if not using Search UI.'
})
]
}
]
}]
};
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
}
npm install
npx heft build --production
npx heft package-solution --production
If your SharePoint environment uses classic pages and your administrator has enabled custom scripts, you can use a Script Editor web part to paste the embed code directly. However, this approach is being phased out by Microsoft and is not recommended for new deployments.
The authentication flow requires a callback page — the page your identity provider redirects users to after sign-in. On SharePoint, we recommend creating a dedicated callback page rather than using the same page that hosts the widget.
Understanding the authentication flow helps explain why the callback page is needed:
The user stays on the callback page after sign-in — they are not automatically returned to the page they started from. This is standard OIDC behaviour.
https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx. You can name it anything (callback, airgentic-login, etc.).On every other page where you add the Airgentic web part, set the Redirect URI property to point to your callback page — not to the page itself.
For example, if your callback page is https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx, then:
| Page | Redirect URI setting |
|---|---|
| Homepage | https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx |
| HR Portal | https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx |
| Callback page | https://yourorg.sharepoint.com/sites/intranet/SitePages/callback.aspx |
All pages point to the same callback URL.
Since users stay on the callback page after authentication, consider making it a page where they'd naturally want to use the widget — such as your main intranet homepage — rather than a blank page.
Alternatively, you can create a simple callback page with a message like "You're now signed in. Return to [Homepage] to continue." with a link back.
Your SharePoint origin must be included in the Airgentic service configuration. When you send your details to Airgentic (see Registering Airgentic in your Identity Provider), include your SharePoint origin:
https://yourorg.sharepoint.com
If your organisation uses a custom domain for SharePoint (e.g. https://intranet.yourorg.gov.au), use that instead.
The Airgentic widget itself uses iframes internally — this is normal and works correctly on SharePoint.
However, if the SharePoint page that hosts the widget is loaded inside another iframe (for example, if someone embeds the SharePoint page within Microsoft Teams using an iframe, or wraps it in a third-party portal), the authentication redirect may fail. Identity providers such as Microsoft Entra ID block their sign-in pages from rendering inside nested iframes.
Ensure the SharePoint page with the Airgentic widget is loaded as a top-level page in the browser, not embedded inside another frame.
If your intranet is accessed through Microsoft Teams or Viva Connections, be aware that these environments can wrap SharePoint pages in ways that interfere with redirect-based authentication. If your users access the intranet through Teams or Viva, test the widget in those environments specifically to confirm the sign-in flow works correctly.
The widget stores authentication state in the browser's session storage, scoped to the top-level window. This means:
If your organisation uses group-based authorisation and users belong to a large number of groups (more than 150), Microsoft Entra ID may not include all groups directly in the token. In this case, Entra ID returns a link to the Microsoft Graph API instead. If you plan to use group-based authorisation, let Airgentic know how many groups your users typically belong to so we can ensure the configuration handles this correctly.
| Step | Action |
|---|---|
| 1 | SharePoint admin adds https://chat.airgentic.com to trusted script sources |
| 2 | Deploy the Airgentic SPFx package (.sppkg) from the App Catalog |
| 3 | Create a dedicated callback page with the Airgentic web part |
| 4 | Register the callback page URL in your Entra ID app registration |
| 5 | Send your SharePoint origin, client ID, client secret, tenant ID, and callback URL to Airgentic |
| 6 | Test sign-in and widget functionality |
| 7 | If accessing via Teams or Viva, test in those environments too |
| 8 | (Optional) For a global hover launcher on all pages: set tenant storage entities and enable the Application Customizer tenant-wide — see Global hover launcher |
chat.airgentic.com. If present, the trusted script source has not been added (Step 1).data-auth-redirect-uri does not exactly match the redirect URI registered in Entra ID. SharePoint page URLs are case-sensitive and include the full path — ensure they match character for character.If you need help, contact Airgentic support — see Contacting Airgentic.