# Creating Apps for Agility 

> Source: https://agilitycms.com/docs/apps/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:

-   http://agilitycms.com/docs/apps/apps-sdk
-   http://agilitycms.com/docs/apps/apps-in-agility

## 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

```bash
npm install @agility/app-sdk
```

Optional (for management API scenarios):

```bash
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):

```json
{
	"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):

```ts
// 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`

```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:

```json
{
	"capabilities": {
		"fields": [{ "name": "example-field", "label": "Example Field", "description": "Custom UI." }]
	}
}
```

### Content Item Sidebar

File: `app/content-item-sidebar/page.tsx`

```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:

```json
{
	"capabilities": {
		"contentItemSidebar": { "description": "Helper tools for content editing." }
	}
}
```

### Page Sidebar

File: `app/page-sidebar/page.tsx`

```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`

```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):

```json
{
	"capabilities": {
		"homeDashboard": { "description": "Instance-level analytics." },
		"contentDashboard": { "description": "Content insights." },
		"pagesDashboard": { "description": "Pages insights." }
	}
}
```

### Modals

File: `app/modals/example-modal/page.tsx`

```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:

```ts
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:

```json
{
	"capabilities": {
		"modals": [{ "name": "example-modal", "label": "Example Modal", "description": "Reusable dialog." }]
	}
}
```

<!-- Removed advanced surfaces (Sections, Items, Flyouts) to keep the guide focused on the most common app patterns. -->

## Install Screen (Optional)

Add a pre‑install screen to collect extra configuration values.

File: `app/install/page.tsx`

```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:

```json
{ "capabilities": { "installScreen": true } }
```

## OAuth (Optional)

Declare OAuth connections in the app definition and implement a matching route.

App definition snippet:

```json
{
	"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`

```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`

```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:

```json
{ "capabilities": { "uninstallHook": "/api/app-uninstall" } }
```

## Local Development

1. Install dependencies:
    ```bash
    npm install
    ```
2. Start dev server (port 3001 in this repo):
    ```bash
    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

-   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.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.
