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.json from 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.json and 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 code
  • appInstallContext: install details including configuration
  • instance: 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

  1. Install dependencies:
    npm install
    
  2. Start dev server (port 3001 in this repo):
    npm run dev
    
  3. Ensure your app definition is reachable at http://localhost:3001/.well-known/agility-app.json.
  4. 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

  1. 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.
  2. Install (Instance): Open your Instance, go to Apps, find your app, and install it. If you enabled an Install Screen, complete the configuration.
  3. Add surfaces: For fields/sections/items, update your content models to reference your app surfaces by name.

Best Practices

  • Wait for initializing === false before 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 name matches the definition and the route exists under app/modals/<name>/page.tsx.
  • CORS blocked: Add headers for /.well-known/agility-app.json (see next.config.js).

Useful References

Summary

  • Define your app via /.well-known/agility-app.json and list only the surfaces you implement.
  • Build surfaces as Next.js routes, use useAgilityAppSDK(), and prefer useResizeHeight() 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.