Skip to main content

App Surfaces

App Surfaces are specific locations within the epilot platform where your custom app can be embedded. Each surface type provides a different context and set of capabilities, allowing you to extend epilot's functionality in targeted ways.

Overviewโ€‹

When you build an app for epilot, it runs inside an iframe embedded within the epilot XRM. The @epilot/app-bridge library enables communication between your app and the parent epilot application, providing:

  • Authentication - OAuth tokens for epilot API calls
  • Localization - Access to the user's language preference
  • Context - Entity, page, or action configuration data
  • Messaging - Two-way communication with the parent app
Install the App Bridge
npm install @epilot/app-bridge

Available Surface Typesโ€‹

Entity Capabilityโ€‹

A collapsible section within an entity detail view. Use this surface to display entity-specific data, metrics, or custom UI elements directly on the entity page.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Entity: Contact - John Doe โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ–ผ Overview โ”‚
โ”‚ Name: John Doe โ”‚
โ”‚ Email: john@example.com โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ–ผ Your App Capability โ† Your App โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ Custom content here โ”‚ โ”‚
โ”‚ โ”‚ (iframe) โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Use cases:

  • Display external data related to an entity
  • Show metrics or analytics
  • Provide quick actions or tools
  • Integrate third-party services

Entity Tabโ€‹

A dedicated tab within the entity detail view. Tabs provide more space than capabilities and are ideal for comprehensive features that require a full-page layout.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Entity: Contact - John Doe โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Overview โ”‚ History โ”‚ Your Tab โ† โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ Your full-page content โ”‚ โ”‚
โ”‚ โ”‚ (iframe) โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Use cases:

  • Complex data visualizations
  • Full-featured integrations
  • Document management
  • Detailed analytics dashboards

Flow Action Configโ€‹

Configuration UI for custom automation actions. When users add your custom action to an automation workflow, this surface displays your configuration interface.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Automation: New Order Processing โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Trigger: Order Created โ”‚
โ”‚ โ†“ โ”‚
โ”‚ Action: Your Custom Action โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ Configuration: โ”‚ โ”‚
โ”‚ โ”‚ Webhook URL: [____________] โ”‚ โ”‚
โ”‚ โ”‚ Enable: [โœ“] โ”‚ โ”‚
โ”‚ โ”‚ (iframe) โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ†“ โ”‚
โ”‚ Action: Send Email โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Use cases:

  • Configure webhook endpoints
  • Set up third-party service credentials
  • Define action parameters
  • Map data fields

Custom Pageโ€‹

A full standalone page within the epilot 360 portal. Custom Pages register their own /app/<slug> route, use the standard 360 layout (sidebar navigation + topbar), and appear as navigation items in the Custom workplace. They support sub-pages and deep-linking.

Use cases:

  • Data dashboards and explorers
  • Admin panels and settings pages
  • Multi-step wizards or onboarding flows
  • Full-page third-party integrations

See also: Custom Page component docs for component configuration details.


Getting Started with App Bridgeโ€‹

Basic Initializationโ€‹

tip

Every app must call initialize() before using any other app-bridge functions. This establishes the communication channel with the parent epilot application.

Basic initialization
import { initialize } from '@epilot/app-bridge';

async function main() {
// Initialize and get authentication data
const { token, lang } = await initialize();

// token: OAuth access token for epilot APIs
// lang: User's language preference ('en', 'de', etc.)

console.log('App initialized with language:', lang);
}

main().catch(console.error);

Initialization Optionsโ€‹

Initialization with options
const session = await initialize({
contentHeight: 400, // Initial iframe height in pixels
timeout: 5000, // Timeout in milliseconds (default: 5000)
});

Entity Surface Implementationโ€‹

For both Entity Capability and Entity Tab surfaces, use the entity-related APIs.

Getting Entity Contextโ€‹

Getting entity context
import { initialize, getEntityContext } from '@epilot/app-bridge';

async function main() {
const { token, lang } = await initialize();

// Get the entity being viewed
const context = await getEntityContext();

console.log('Entity ID:', context.entityId);
console.log('Schema:', context.schema); // e.g., 'contact', 'order'
console.log('Capability:', context.capability); // Your app's capability config
}

Context Parameters:

ParameterTypeDescription
entityIdstringUUID of the entity being viewed
schemastringEntity schema slug (e.g., 'contact', 'order', 'opportunity')
capabilityEntityCapabilityCapability configuration from your app manifest
isVisiblebooleanWhether the tab/capability is currently visible (tabs only)

Dynamic Content Heightโ€‹

Since your app runs in an iframe, you need to communicate your content height to epilot so it can adjust the iframe size appropriately.

Report content height
import { initialize, updateContentHeight } from '@epilot/app-bridge';

async function main() {
await initialize();

// After rendering your content
renderContent();

// Report the content height
updateContentHeight(document.body.scrollHeight);
}

For dynamic content, use ResizeObserver:

Auto-update height with ResizeObserver
import { initialize, updateContentHeight } from '@epilot/app-bridge';

async function main() {
await initialize();

const container = document.getElementById('app');

// Automatically update height when content changes
const observer = new ResizeObserver((entries) => {
const height = entries[0].contentRect.height;
updateContentHeight(height);
});

observer.observe(container);
}

Handling Tab Visibilityโ€‹

For Entity Tab surfaces, the tab may be hidden when the user switches to another tab. Handle visibility changes to optimize performance:

Handle tab visibility changes
import {
initialize,
getEntityContext,
onVisibilityChange
} from '@epilot/app-bridge';

async function main() {
await initialize();

const { entityId, schema, isVisible } = await getEntityContext();

// Initial load only if visible
if (isVisible) {
await loadData(entityId, schema);
}

// Subscribe to visibility changes
const unsubscribe = onVisibilityChange((visible) => {
if (visible) {
// Tab became visible - refresh data
refreshData();
}
});

// Clean up when your app unmounts
// unsubscribe();
}

Action Config Implementationโ€‹

For Flow Action Config surfaces, use the action configuration APIs.

Reading Action Configurationโ€‹

Read action config
import { initialize, getActionConfig } from '@epilot/app-bridge';

// Define your configuration type
interface WebhookConfig {
url: string;
headers: Record<string, string>;
enabled: boolean;
}

async function main() {
await initialize();

// Get existing configuration
const config = await getActionConfig<WebhookConfig>();

// Access your custom config
const webhookUrl = config.custom_action_config?.url ?? '';
const isEnabled = config.custom_action_config?.enabled ?? false;

// Render your configuration form
renderForm({ webhookUrl, isEnabled });
}

Config Parameters:

ParameterTypeDescription
custom_action_configTYour custom configuration object
descriptionstring?Action description shown in the UI
app_idstring?Associated app ID

Updating Action Configurationโ€‹

When the user changes configuration in your UI, update it immediately:

Update action config on change
import { initialize, getActionConfig, updateActionConfig } from '@epilot/app-bridge';

interface WebhookConfig {
url: string;
enabled: boolean;
}

async function main() {
await initialize();

const { custom_action_config } = await getActionConfig<WebhookConfig>();

// Set up form with existing values
const urlInput = document.getElementById('webhook-url') as HTMLInputElement;
urlInput.value = custom_action_config?.url ?? '';

// Update config when user makes changes
urlInput.addEventListener('change', () => {
updateActionConfig<WebhookConfig>({
url: urlInput.value,
enabled: custom_action_config?.enabled ?? true,
});
});
}

Async Actions with Callbacksโ€‹

If your action performs asynchronous work and the automation should wait for it to complete, use waitForCallback:

Enable async callback
updateActionConfig(
{ url: 'https://api.example.com/webhook' },
{ waitForCallback: true }
);

When waitForCallback is true, the automation engine will wait for your action to signal completion before proceeding to the next step.


Page Surface Implementationโ€‹

For Custom Page surfaces, use the page-related APIs. These APIs let you read the current page context, navigate between sub-pages, and respond to browser back/forward navigation.

Getting Page Contextโ€‹

Get page context
import { initialize, getPageContext } from '@epilot/app-bridge';

async function main() {
const { token, lang } = await initialize();

// Get the page context
const { slug, subPath, path } = await getPageContext();

console.log('Page slug:', slug); // e.g., "energy-prices"
console.log('Sub-path:', subPath); // e.g., "/dashboard"
console.log('Full path:', path); // e.g., "/app/energy-prices/dashboard"

// Render based on current sub-path
renderPage(subPath);
}

Context Parameters:

ParameterTypeDescription
slugstringThe page slug from the URL (e.g., 'energy-prices')
subPathstringSub-path after the slug (e.g., '/dashboard', '/settings/advanced')
pathstringFull URL path (e.g., '/app/energy-prices/dashboard')

Sub-Page Navigationโ€‹

Use navigate() to change the parent frame URL when users interact with your app's internal navigation. This enables deep-linking and updates the browser address bar.

Navigate between sub-pages
import { navigate } from '@epilot/app-bridge';

// Navigate to sub-pages within your app
navigate('/dashboard'); // URL becomes /app/<slug>/dashboard
navigate('/settings/advanced'); // URL becomes /app/<slug>/settings/advanced
navigate('/'); // URL becomes /app/<slug>
tip

navigate() triggers a history.pushState in the parent frame. It does not reload the page or the iframe - your app stays mounted. Update your UI in response to the navigation.

Handling Browser Back/Forwardโ€‹

When the user clicks the browser back or forward button, the parent frame detects the popstate event and notifies your app:

React to browser navigation
import { onLocationChange } from '@epilot/app-bridge';

const unsubscribe = onLocationChange((subPath) => {
console.log('Browser navigated to:', subPath);
renderPage(subPath);
});

// Clean up when your app unmounts
// unsubscribe();

Combining Navigate and Location Changeโ€‹

A typical pattern is to use navigate() for user-initiated navigation and onLocationChange() for browser-initiated navigation:

Full navigation pattern
import {
initialize,
getPageContext,
navigate,
onLocationChange,
updateContentHeight,
} from '@epilot/app-bridge';

async function main() {
await initialize();
const { subPath } = await getPageContext();

// Initial render
renderPage(subPath);

// Handle browser back/forward
onLocationChange((newSubPath) => {
renderPage(newSubPath);
});

// Handle user clicks on your internal nav
document.addEventListener('click', (e) => {
const link = (e.target as HTMLElement).closest('[data-route]');
if (link) {
e.preventDefault();
const route = link.getAttribute('data-route')!;
navigate(route);
renderPage(route);
}
});
}

function renderPage(subPath: string) {
// Your routing logic here
updateContentHeight(document.body.scrollHeight);
}

Authorizing API Clientsโ€‹

The app-bridge provides a convenient way to authorize epilot SDK clients:

Authorize SDK clients
import { getClient } from '@epilot/entity-client';
import { getClient as getFileClient } from '@epilot/file-client';
import { initialize, authorizeClient } from '@epilot/app-bridge';

async function main() {
const session = await initialize();

// Create clients
const entityClient = getClient();
const fileClient = getFileClient();

// Authorize with the session
authorizeClient(entityClient, session);
authorizeClient(fileClient, session);

// Now ready to make API calls
const entities = await entityClient.searchEntities({
schema: 'contact',
query: 'john',
});
}

You can also pass just the token string:

authorizeClient(entityClient, session.token);

Custom Eventsโ€‹

For advanced use cases, you can send and receive custom events:

Sending Eventsโ€‹

import { send } from '@epilot/app-bridge';

// Send a custom event to the parent
send('my-custom-event', {
action: 'refresh',
data: { key: 'value' },
});

Receiving Eventsโ€‹

import { on } from '@epilot/app-bridge';

// Listen for custom events
const unsubscribe = on<{ action: string }>('parent-event', (data) => {
console.log('Received action:', data.action);
});

// Cleanup when done
unsubscribe();

Wildcard Subscriptionsโ€‹

// Listen to all events
on('*', (data) => {
console.log('Event received:', data);
});

// Listen to events with a prefix
on('custom-*', (data) => {
console.log('Custom event:', data);
});

Error Handlingโ€‹

The app-bridge provides specific error types for better error handling:

import {
initialize,
getSession,
AppBridgeTimeoutError,
AppBridgeNotInitializedError,
} from '@epilot/app-bridge';

// Handle initialization timeout
try {
await initialize({ timeout: 3000 });
} catch (error) {
if (error instanceof AppBridgeTimeoutError) {
console.error(`Initialization timed out after ${error.timeout}ms`);
showError('Failed to connect to epilot. Please refresh the page.');
}
}

// Handle not initialized error
try {
const session = getSession();
} catch (error) {
if (error instanceof AppBridgeNotInitializedError) {
// App bridge not initialized yet
await initialize();
}
}

Complete Examplesโ€‹

Entity Capability Appโ€‹

A complete example of an app that displays external CRM data for a contact:

Entity Capability - CRM integration example
import {
initialize,
getEntityContext,
updateContentHeight,
authorizeClient,
} from '@epilot/app-bridge';
import { getClient } from '@epilot/entity-client';

interface ExternalCRMData {
score: number;
lastContact: string;
notes: string[];
}

async function main() {
// 1. Initialize app bridge
const { token, lang } = await initialize();

// 2. Set up API client
const entityClient = getClient();
authorizeClient(entityClient, token);

// 3. Set language
document.documentElement.lang = lang;

// 4. Get entity context
const { entityId, schema } = await getEntityContext();

// 5. Fetch entity data
const entity = await entityClient.getEntity({ slug: schema, id: entityId });

// 6. Fetch external CRM data (your API)
const externalData = await fetchExternalCRM(entity.data.email);

// 7. Render UI
renderCRMWidget(externalData);

// 8. Update content height
const observer = new ResizeObserver((entries) => {
updateContentHeight(entries[0].contentRect.height);
});
observer.observe(document.getElementById('app')!);
}

function renderCRMWidget(data: ExternalCRMData) {
const app = document.getElementById('app')!;
app.innerHTML = `
<div class="crm-widget">
<div class="score">Score: ${data.score}</div>
<div class="last-contact">Last Contact: ${data.lastContact}</div>
<div class="notes">
${data.notes.map(note => `<p>${note}</p>`).join('')}
</div>
</div>
`;
}

async function fetchExternalCRM(email: string): Promise<ExternalCRMData> {
const response = await fetch(`https://api.mycrm.com/lookup?email=${email}`);
return response.json();
}

main().catch(console.error);

Entity Tab with Visibility Handlingโ€‹

An app that displays a dashboard, refreshing data when the tab becomes visible:

Entity Tab - Dashboard with visibility handling
import {
initialize,
getEntityContext,
onVisibilityChange,
updateContentHeight,
} from '@epilot/app-bridge';

async function main() {
const { token, lang } = await initialize();
const { entityId, schema, isVisible } = await getEntityContext();

// Initial setup
await setupDashboard(entityId, schema);

// Load data if tab is already visible
if (isVisible) {
await loadDashboardData(entityId);
}

// Refresh data when tab becomes visible
onVisibilityChange(async (visible) => {
if (visible) {
await loadDashboardData(entityId);
}
});

// Update height
updateContentHeight(document.body.scrollHeight);
}

async function setupDashboard(entityId: string, schema: string) {
// Initialize dashboard UI
}

async function loadDashboardData(entityId: string) {
// Fetch and render dashboard data
}

main().catch(console.error);

Automation Action Configurationโ€‹

A complete configuration UI for a webhook action:

Flow Action Config - Webhook configuration UI
import {
initialize,
getActionConfig,
updateActionConfig,
updateContentHeight,
} from '@epilot/app-bridge';

interface WebhookActionConfig {
url: string;
method: 'GET' | 'POST' | 'PUT';
headers: Record<string, string>;
retryCount: number;
}

async function main() {
await initialize();

// Get existing configuration
const { custom_action_config } = await getActionConfig<WebhookActionConfig>();
const config = custom_action_config ?? {
url: '',
method: 'POST',
headers: {},
retryCount: 3,
};

// Render configuration form
renderForm(config);
updateContentHeight(document.body.scrollHeight);
}

function renderForm(config: WebhookActionConfig) {
const app = document.getElementById('app')!;
app.innerHTML = `
<form id="config-form">
<div class="field">
<label>Webhook URL</label>
<input type="url" id="url" value="${config.url}" required />
</div>
<div class="field">
<label>HTTP Method</label>
<select id="method">
<option value="GET" ${config.method === 'GET' ? 'selected' : ''}>GET</option>
<option value="POST" ${config.method === 'POST' ? 'selected' : ''}>POST</option>
<option value="PUT" ${config.method === 'PUT' ? 'selected' : ''}>PUT</option>
</select>
</div>
<div class="field">
<label>Retry Count</label>
<input type="number" id="retryCount" value="${config.retryCount}" min="0" max="10" />
</div>
</form>
`;

// Listen for changes
const form = document.getElementById('config-form')!;
form.addEventListener('change', () => {
const newConfig: WebhookActionConfig = {
url: (document.getElementById('url') as HTMLInputElement).value,
method: (document.getElementById('method') as HTMLSelectElement).value as 'GET' | 'POST' | 'PUT',
headers: config.headers,
retryCount: parseInt((document.getElementById('retryCount') as HTMLInputElement).value),
};
updateActionConfig(newConfig);
});
}

main().catch(console.error);

Custom Page Appโ€‹

A complete example of a custom page app with sub-page navigation:

Custom Page - Multi-page app with navigation
import {
initialize,
getPageContext,
navigate,
onLocationChange,
updateContentHeight,
authorizeClient,
} from '@epilot/app-bridge';
import { getClient } from '@epilot/entity-client';

type Route = '/' | '/dashboard' | '/settings';

async function main() {
// 1. Initialize app bridge
const { token, lang } = await initialize();

// 2. Set up API client
const entityClient = getClient();
authorizeClient(entityClient, token);

// 3. Get page context
const { slug, subPath } = await getPageContext();

// 4. Render initial page
renderPage(subPath as Route);

// 5. Handle browser back/forward
onLocationChange((newSubPath) => {
renderPage(newSubPath as Route);
});

// 6. Set up internal navigation
setupNavigation();
}

function setupNavigation() {
document.addEventListener('click', (e) => {
const link = (e.target as HTMLElement).closest('[data-route]');
if (link) {
e.preventDefault();
const route = link.getAttribute('data-route') as Route;
navigate(route);
renderPage(route);
}
});
}

function renderPage(route: Route) {
const app = document.getElementById('app')!;

const nav = `
<nav>
<a data-route="/" ${route === '/' ? 'class="active"' : ''}>Home</a>
<a data-route="/dashboard" ${route === '/dashboard' ? 'class="active"' : ''}>Dashboard</a>
<a data-route="/settings" ${route === '/settings' ? 'class="active"' : ''}>Settings</a>
</nav>
`;

switch (route) {
case '/dashboard':
app.innerHTML = nav + '<h1>Dashboard</h1><p>Your data here</p>';
break;
case '/settings':
app.innerHTML = nav + '<h1>Settings</h1><p>Configuration options</p>';
break;
default:
app.innerHTML = nav + '<h1>Welcome</h1><p>Choose a section above</p>';
}

updateContentHeight(document.body.scrollHeight);
}

main().catch(console.error);
Sample App

For a complete, production-ready example of a Custom Page app, see the Energy Spot Price Explorer sample:

github.com/epilot-dev/epilot-app-sample-energy-prices


API Referenceโ€‹

Session Managementโ€‹

FunctionReturnsDescription
initialize(options?)Promise<AppBridgeSession>Initialize the app bridge and establish the postMessage channel with the parent epilot app. Returns session data with an OAuth token and the user's language. Safe to call multiple times - subsequent calls return the cached session.
getSession()AppBridgeSessionGet the cached session synchronously. Throws AppBridgeNotInitializedError if called before initialize().
isInitialized()booleanCheck whether the app bridge has been initialized.

InitOptions:

OptionTypeDefaultDescription
contentHeightnumberdocument.body.scrollHeightInitial iframe height in pixels to report to the parent
timeoutnumber5000Timeout in milliseconds for the initialization handshake

Entity Surface APIโ€‹

FunctionReturnsDescription
getEntityContext(options?)Promise<EntityContext>Get the entity being viewed. Available on Entity Capability and Entity Tab surfaces.
updateContentHeight(height)voidReport your content height to the parent so it can resize the iframe. Call after rendering or when content size changes.
onVisibilityChange(handler)UnsubscribeSubscribe to visibility changes on Entity Tab surfaces. The handler receives true when the tab becomes visible, false when hidden. Returns an unsubscribe function.

Action Config APIโ€‹

FunctionReturnsDescription
getActionConfig<T>(options?)Promise<ActionConfig<T>>Get the current action configuration. Available on Flow Action Config surfaces.
updateActionConfig<T>(config, options?)voidUpdate the action configuration. The parent automation UI receives the new config immediately.

UpdateConfigOptions:

OptionTypeDefaultDescription
waitForCallbackbooleanfalseIf true, the automation engine waits for an async callback before proceeding to the next action

Page Surface APIโ€‹

FunctionReturnsDescription
getPageContext(options?)Promise<PageContext>Get the page context including slug, sub-path, and full path. Available on Custom Page surfaces.
navigate(subPath)voidNavigate to a sub-path within the current page. Triggers history.pushState in the parent frame, updating the browser URL. Does not reload the iframe.
onLocationChange(handler)UnsubscribeSubscribe to browser back/forward navigation. The handler receives the new sub-path. Returns an unsubscribe function.

Generic Event APIโ€‹

FunctionReturnsDescription
on<T>(event, handler)UnsubscribeSubscribe to events from the parent app. Supports wildcard patterns (e.g., 'custom-*' or '*'). Returns an unsubscribe function.
send(event, data?)voidSend a custom event to the parent app. Use for custom communication not covered by the high-level APIs.

Client Authorizationโ€‹

FunctionReturnsDescription
authorizeClient(client, sessionOrToken)voidAuthorize an @epilot/*-client SDK client with the session token. Accepts either an AppBridgeSession object or a token string. Sets the Authorization: Bearer <token> header on the client.

Low-Level Messaging APIโ€‹

For advanced use cases, the epilot object provides direct access to the postMessage transport:

import { epilot } from '@epilot/app-bridge';

// Send a raw message to the parent
epilot.sendMessageToParent('my-event', { key: 'value' });

// Subscribe to raw messages from the parent (supports wildcards)
const unsubscribe = epilot.subscribeToParentMessages('my-event', (data) => {
console.log('Received:', data);
});
FunctionReturnsDescription
epilot.sendMessageToParent(event, detail?)voidSend a raw postMessage to the parent window
epilot.subscribeToParentMessages(event, handler)UnsubscribeSubscribe to raw postMessage events from the parent. Supports wildcard matching with *.

Typesโ€‹

import type {
// Session
AppBridgeSession, // { token: string; lang: string }
InitOptions, // { contentHeight?: number; timeout?: number }
RequestOptions, // { timeout?: number }

// Entity Surface
EntityContext, // { entityId: string; schema: string; capability?: EntityCapability; isVisible?: boolean }
EntityCapability, // { name?: string; app_id?: string; [key: string]: unknown }

// Page Surface
PageContext, // { slug: string; subPath: string; path: string }

// Action Config Surface
ActionConfig<T>, // { custom_action_config?: T; description?: string; app_id?: string }
UpdateConfigOptions, // { waitForCallback?: boolean }

// Event Handlers
MessageHandler<T>, // (data: T) => void
VisibilityHandler, // (isVisible: boolean) => void
Unsubscribe, // () => void
} from '@epilot/app-bridge';

Error Typesโ€‹

import {
AppBridgeError, // Base error class for all app-bridge errors
AppBridgeTimeoutError, // Thrown when a request times out (has .event and .timeout properties)
AppBridgeNotInitializedError // Thrown when calling functions before initialize()
} from '@epilot/app-bridge';
ErrorPropertiesDescription
AppBridgeErrormessageBase error class. All app-bridge errors extend this.
AppBridgeTimeoutErrorevent: string, timeout: numberA request/response operation timed out. The event property identifies which operation failed and timeout is the duration in ms.
AppBridgeNotInitializedError-getSession() or other functions were called before initialize().

Best Practicesโ€‹

caution

Always wrap initialize() in a try-catch block. If the app bridge fails to connect (e.g., the app is loaded outside of epilot), your app should display a user-friendly error message rather than silently failing.

  1. Always initialize first - Call initialize() before any other app-bridge functions
  2. Handle errors gracefully - Wrap initialization in try-catch and show user-friendly errors
  3. Update content height - Always report your content height, especially after rendering
  4. Use ResizeObserver - For dynamic content, observe size changes automatically
  5. Handle visibility - For tabs, pause expensive operations when not visible
  6. Type your configs - Use TypeScript generics for type-safe configuration
  7. Clean up subscriptions - Call unsubscribe functions when your app unmounts