See Agility CMS in action. Watch a product demo
Creating Apps for Agility
Build custom functionality that runs inside Agility CMS using the Apps SDK v2. This guide covers the end‑to‑end flow: planning your app, defining capabilities, implementing UI surfaces, local development, OAuth, installation, and best practices.
You only implement the surfaces you need. An app can be a single custom field, a sidebar, a dashboard, one or more modals, or a combination.
This guide is hosted at http://agilitycms.com/docs/apps/creating-apps-for-agility. For related topics, see:
What You Can Build
- Custom Fields: bespoke editors that read/write field values.
- this is the most common use case for apps in Agility CMS.
- Modals: reusable dialogs launched from other surfaces.
- these are often used in conjunction with custom fields as a "picker" screen. Ex: a product or asset picker.
- Sidebars: contextual tools for content lists, content items, and pages.
- Dashboards: Home, Content, and Pages dashboards for analytics and actions.
Prerequisites
- Node.js 18+ and npm
- React 18+ with TypeScript recommended
- A React build setup (Vite, CRA, Next.js, etc.). We recommend Next.js as a production‑ready React framework, but any React app works with the SDK.
- An Agility CMS Organization (to register apps) and Instance (to install apps)
Example App
We provide a complete “kitchen sink” example you can run and use as a reference:
- Scripts:
npm run dev,npm run build,npm start - App definition served at
/.well-known/agility-app.json(ensure CORS is enabled in your framework/server) - In a Next.js setup, surfaces live under an
app/folder. If you’re using a different React setup (Vite/CRA), map each surface to a routable React component path your router can render.
Install the SDK
npm install @agility/app-sdk
Optional (for management API scenarios):
npm install @agility/management-sdk
App Definition File
Every app must expose /.well-known/agility-app.json. This file describes your app’s metadata, configuration options, OAuth connections, and the surfaces it provides.
Minimal example (only a custom field):
{
"name": "My Custom Field",
"version": "1.0.0",
"__sdkVersion": "2.0.0",
"capabilities": {
"fields": [{ "name": "my-field", "label": "My Field", "description": "Custom editor UI." }]
}
}
Kitchen‑sink example tips:
- Place your app definition at
public/.well-known/agility-app.json(or serve it via middleware) - Ensure you set CORS headers so Agility can fetch the definition from any origin (e.g., via Next.js
headers()or your server config)
Project Structure (React)
Your app is a React application. Each surface should be a routable React component/page that the Agility CMS UI can load in an iframe.
If you use Next.js, each surface is a route under app/ (App Router):
- Custom Field:
app/fields/<field-name>/page.tsx - Content List Sidebar:
app/content-list-sidebar/page.tsx - Content Item Sidebar:
app/content-item-sidebar/page.tsx - Page Sidebar:
app/page-sidebar/page.tsx - Dashboards:
app/home-dashboard/page.tsx,app/content-dashboard/page.tsx,app/pages-dashboard/page.tsx - Modals:
app/modals/<modal-name>/page.tsx - (Optional) Advanced surfaces like sections/items/flyouts are available but not required for most apps.
- Install Screen:
app/install/page.tsx - Uninstall Hook (API):
app/api/app-uninstall/route.ts(or an API endpoint in your chosen framework) - OAuth:
app/oauth/<connection>/page.tsx
If you use Vite/CRA or a custom React setup:
- Ensure your router exposes stable paths for each surface (e.g.,
/fields/my-field,/modals/example-modal). - Serve the app definition at
/.well-known/agility-app.jsonfrom your static assets or an express middleware. - Example (Vite + express middleware):
// server.js (optional express for local dev)
import express from "express"
import fs from "fs"
import path from "path"
const app = express()
app.get("/.well-known/agility-app.json", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*")
const json = fs.readFileSync(path.join(process.cwd(), "public/.well-known/agility-app.json"), "utf-8")
res.type("application/json").send(json)
})
app.use(express.static("dist"))
app.listen(3001, () => console.log("App running on http://localhost:3001"))
- Point Agility to your public base URL; it must resolve
/.well-known/agility-app.jsonand each surface route.
Core Hook: useAgilityAppSDK()
In each surface, call useAgilityAppSDK() to access context and SDK methods. Common properties:
initializing: boolean initialization state (render nothing until false)locale: current locale codeappInstallContext: install details includingconfigurationinstance: current instance metadata
Additional properties per surface:
- Field:
field,fieldValue,contentItem - Content Item Sidebar:
contentItem - Page Sidebar:
pageItem - Modal:
modalProps
Implementing Surfaces
Below are minimal React starters you can copy and expand. Examples use Next.js files, but the components work the same in any React router—adjust file paths accordingly.
Custom Field
File: app/fields/example-field/page.tsx
"use client"
import { useAgilityAppSDK, contentItemMethods, useResizeHeight } from "@agility/app-sdk"
export default function ExampleField() {
const { initializing, fieldValue } = useAgilityAppSDK()
const containerRef = useResizeHeight(10)
if (initializing) return null
return (
<div ref={containerRef}>
<textarea
className="w-full rounded border border-gray-300 p-4"
value={(fieldValue as string) || ""}
onChange={(e) => contentItemMethods.setFieldValue({ value: e.target.value })}
/>
</div>
)
}
Add to app definition:
{
"capabilities": {
"fields": [{ "name": "example-field", "label": "Example Field", "description": "Custom UI." }]
}
}
Content Item Sidebar
File: app/content-item-sidebar/page.tsx
"use client"
import { useAgilityAppSDK, contentItemMethods } from "@agility/app-sdk"
import { useEffect, useState } from "react"
export default function ContentItemSidebar() {
const { initializing, contentItem } = useAgilityAppSDK()
const [heading, setHeading] = useState<string>("")
useEffect(() => {
if (!contentItem?.values?.Heading) return
setHeading(contentItem.values.Heading as string)
}, [contentItem])
if (initializing) return null
return (
<div className="p-3">
<p>Heading: {heading}</p>
<button onClick={() => contentItemMethods.saveContentItem()}>Save</button>
<button
onClick={() =>
contentItemMethods.addFieldListener({
fieldName: "Heading",
onChange: (val) => setHeading(val as string)
})
}
>
Listen to Heading
</button>
</div>
)
}
Add to app definition:
{
"capabilities": {
"contentItemSidebar": { "description": "Helper tools for content editing." }
}
}
Page Sidebar
File: app/page-sidebar/page.tsx
"use client"
import { useAgilityAppSDK, pageMethods } from "@agility/app-sdk"
import { useState } from "react"
export default function PageSidebar() {
const { initializing } = useAgilityAppSDK()
const [page, setPage] = useState<any>()
if (initializing) return null
return (
<div>
<button onClick={async () => setPage(await pageMethods.getPageItem())}>Get Page</button>
<pre className="whitespace-pre-wrap break-words text-xs">{JSON.stringify(page, null, 2)}</pre>
</div>
)
}
Dashboards
Files: app/home-dashboard/page.tsx, app/content-dashboard/page.tsx, app/pages-dashboard/page.tsx
"use client"
import { useAgilityAppSDK, useResizeHeight, assetsMethods } from "@agility/app-sdk"
export default function Dashboard() {
const { initializing, locale, appInstallContext } = useAgilityAppSDK()
const ref = useResizeHeight()
if (initializing) return null
return (
<div ref={ref} className="p-4">
<h1 className="text-xl font-semibold">Dashboard</h1>
<div>Locale: {locale}</div>
<div>Config: {JSON.stringify(appInstallContext?.configuration)}</div>
<button
onClick={() =>
assetsMethods.selectAssets({
title: "Select Assets",
singleSelectOnly: false,
callback: console.log
})
}
>
Select Assets
</button>
</div>
)
}
Capabilities (choose which):
{
"capabilities": {
"homeDashboard": { "description": "Instance-level analytics." },
"contentDashboard": { "description": "Content insights." },
"pagesDashboard": { "description": "Pages insights." }
}
}
Modals
File: app/modals/example-modal/page.tsx
"use client"
import { closeModal, useAgilityAppSDK } from "@agility/app-sdk"
export default function ExampleModal() {
const { initializing, modalProps } = useAgilityAppSDK()
if (initializing) return <div>Initializing...</div>
return (
<div className="flex h-full flex-col gap-3 p-3">
<h2 className="text-lg font-semibold">Example Modal</h2>
<div className="flex-1">Props: {JSON.stringify(modalProps)}</div>
<div className="flex gap-2">
<button onClick={() => closeModal({ btn: "ok" })}>OK</button>
<button onClick={() => closeModal({ btn: "cancel" })}>Cancel</button>
</div>
</div>
)
}
Open from any surface:
import { openModal } from "@agility/app-sdk"
openModal({
title: "Example Modal",
name: "example-modal",
props: { foo: "bar" },
callback: (result) => console.log("Modal result:", result)
})
Add to app definition:
{
"capabilities": {
"modals": [{ "name": "example-modal", "label": "Example Modal", "description": "Reusable dialog." }]
}
}
Install Screen (Optional)
Add a pre‑install screen to collect extra configuration values.
File: app/install/page.tsx
"use client"
import { setExtraConfigValues, useAgilityPreInstall } from "@agility/app-sdk"
export default function Install() {
const { initializing } = useAgilityPreInstall()
if (initializing) return null
return (
<div>
<h1>Install</h1>
<button onClick={() => setExtraConfigValues([{ name: "apiKey", value: "xyz123" }])}>
Complete Install
</button>
</div>
)
}
Enable in app definition:
{ "capabilities": { "installScreen": true } }
OAuth (Optional)
Declare OAuth connections in the app definition and implement a matching route.
App definition snippet:
{
"connections": [
{
"name": "Agility API Offline Access",
"icon": "https://cdn.aglty.io/content-manager/images/logo-triangle-only-yellow.svg",
"url": "/oauth/agility-api-offline"
}
]
}
Route: app/oauth/agility-api-offline/page.tsx
"use client"
import { useEffect } from "react"
export default function AgilityAPI() {
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const code = params.get("code")
const redirect_uri = params.get("redirect_uri")
if (code && redirect_uri) {
const formData = new FormData()
formData.append("code", code)
fetch("https://mgmt.aglty.io/oauth/token", { method: "POST", body: formData })
.then((r) => r.text())
.then((token) => (window.location.href = `${redirect_uri}#${encodeURIComponent(token)}`))
} else {
const authUrl = `https://mgmt.aglty.io/oauth/authorize?response_type=code&redirect_uri=${encodeURIComponent(
window.location.href
)}&scope=offline_access`
window.location.href = authUrl
}
}, [])
return <div>Authenticating...</div>
}
Uninstall Hook
Handle cleanup when an app is uninstalled.
File: app/api/app-uninstall/route.ts
export async function POST(request: Request) {
const body = await request.json()
// Clean up resources, stored data, and tokens
return new Response("OK", { status: 200 })
}
Enable in app definition:
{ "capabilities": { "uninstallHook": "/api/app-uninstall" } }
Local Development
- Install dependencies:
npm install - Start dev server (port 3001 in this repo):
npm run dev - Ensure your app definition is reachable at
http://localhost:3001/.well-known/agility-app.json. - If installing into Agility from the cloud, expose your dev server via a tunnel (e.g., ngrok) and use the public URL when registering the app.
Register and Install Your App
- Register (Org Admin): In Agility CMS, create an Organization App and set the App Definition URL to your deployed base URL +
/.well-known/agility-app.json. - Install (Instance): Open your Instance, go to Apps, find your app, and install it. If you enabled an Install Screen, complete the configuration.
- Add surfaces: For fields/sections/items, update your content models to reference your app surfaces by name.
Best Practices
- Wait for
initializing === falsebefore rendering UI. - Use
useResizeHeight()in fields and dashboards for automatic iframe sizing. - Keep surfaces modular; implement only what you need in the app definition.
- Clean up listeners (e.g., remove field/selection listeners on unmount).
- Gracefully handle errors for all SDK calls.
- Keep your app definition URL stable and CORS‑accessible.
Troubleshooting
- The app doesn’t show up: Verify the App Definition URL is reachable and valid JSON, and capabilities match your implemented routes.
- Field value doesn’t save: Ensure you call
contentItemMethods.setFieldValue()and that the field name matches the model. - Modal doesn’t open: Confirm the modal
namematches the definition and the route exists underapp/modals/<name>/page.tsx. - CORS blocked: Add headers for
/.well-known/agility-app.json(seenext.config.js).
Useful References
- Official docs: http://agilitycms.com/docs/apps/creating-apps-for-agility
- SDK reference: http://agilitycms.com/docs/apps/apps-sdk
- Apps overview: http://agilitycms.com/docs/apps/apps-in-agility
- Example apps (source code):
- CommerceTools Product Picker (custom field with modal): https://github.com/agility/agility-cms-app-commercetools
- PostHog integration (sidebar app): https://github.com/agility/agility-cms-app-posthog
Summary
- Define your app via
/.well-known/agility-app.jsonand list only the surfaces you implement. - Build surfaces as Next.js routes, use
useAgilityAppSDK(), and preferuseResizeHeight()where embedded. - Use modals and asset selectors to compose richer flows.
- Register at the Organization level, then install into Instances.
- Start simple; expand capabilities as your use cases grow.