# App definition anatomy

> Every top-level key of a Kustomer app definition, with types, validation rules, naming conventions and a complete example.

Source: https://developer.kustomer.com/docs/apps/app-definition

An app definition is one JSON object. Three keys are required (`app`, `version` and `title`); everything else is optional and adds a kind of component. The schema is strict: a top-level key Kustomer doesn't recognize fails registration rather than being ignored.

_An interactive map from each top-level app definition key to the part of Kustomer it affects, such as the App Directory, the settings page, the timeline, the insights panel, a widget, or behind-the-scenes workflows, hooks, webhooks and commands._

## A complete example

This app syncs orders from a fictional store, Acme. It stores orders as a custom object, shows recent orders in the insights panel, receives order updates on an inbound hook, and lets workflows issue refunds through Acme's API with each org's own key.

```json title="acme_orders.json"
{
  "app": "acme_orders",
  "version": "1.4.0",
  "title": "Acme Orders",
  "identifier": "acmeord",
  "visibility": "private",
  "description": "See Acme orders on every customer and refund them from a workflow.\n\nSends the order ID and refund amount to Acme when a workflow issues a refund.",
  "iconUrl": "https://acme.example/kustomer/icon.png",
  "screenshots": ["https://acme.example/kustomer/screenshot-1.jpg"],
  "appDetails": {
    "appDeveloper": { "name": "Acme", "website": "https://acme.example", "supportEmail": "support@acme.example" },
    "externalPlatform": { "name": "Acme Commerce", "website": "https://acme.example/commerce" },
    "documentationLinks": [{ "title": "Set up Acme for Kustomer", "url": "https://acme.example/docs/kustomer" }]
  },
  "tags": ["ecommerce"],
  "dependencies": ["kustomer-^1.9.0"],
  "settings": {
    "default": {
      "apiKey": { "type": "secret", "defaultValue": "", "required": true },
      "storeDomain": { "type": "string", "defaultValue": "", "required": true }
    }
  },
  "klasses": [
    {
      "name": "acme_orders-order",
      "icon": "cart",
      "color": "#5B6CF9",
      "metadata": {
        "properties": {
          "orderNumberStr": { "displayName": "Order number" },
          "statusStr": { "displayName": "Status" },
          "totalNum": { "displayName": "Total" },
          "trackingUrl": { "displayName": "Tracking" },
          "placedAt": { "displayName": "Placed" }
        }
      }
    }
  ],
  "kviews": [
    {
      "name": "kustomer.app.acme_orders.order-card",
      "resource": "kobject.acme_orders-order",
      "context": "smartbar-card",
      "meta": { "displayName": "Acme order", "icon": "cart" },
      "template": "<Segment><BasicRow label=\"Status\" value={object.custom.statusStr} /><BasicRow label=\"Total\" value={object.custom.totalNum} /></Segment>"
    }
  ],
  "hooks": [
    {
      "eventName": "kustomer.app.acme_orders.order-updated",
      "displayEventName": "Acme order updated",
      "description": "Acme posts here whenever an order changes.",
      "type": "web"
    }
  ],
  "triggers": [
    {
      "eventName": "kustomer.app.acme_orders.order-updated",
      "description": "An order changed in Acme",
      "outputTemplate": {
        "orderId": "/#data.attributes.data.id",
        "email": "/#data.attributes.data.customer.email",
        "status": "/#data.attributes.data.status"
      },
      "outputSchema": {
        "type": "object",
        "properties": { "orderId": { "type": "string" }, "email": { "type": "string" }, "status": { "type": "string" } },
        "additionalProperties": false
      },
      "sampleOutputContext": { "orderId": "4417", "email": "ada@example.com", "status": "shipped" }
    }
  ],
  "actions": [
    {
      "name": "kustomer.app.acme_orders.refund",
      "description": "Refund an Acme order",
      "type": "rest_api",
      "appSettings": { "apiKey": { "key": "acme_orders.default.apiKey" } },
      "inputTemplate": {
        "uri": "https://api.acme.example/orders/{{orderId}}/refunds",
        "method": "POST",
        "headers": { "Authorization": "Bearer {{apiKey}}", "Content-Type": "application/json" },
        "data": { "amount": "/#amount" },
        "json": true
      },
      "inputSchema": {
        "type": "object",
        "properties": { "orderId": { "type": "string" }, "amount": { "type": "number" } },
        "required": ["orderId"],
        "additionalProperties": false
      },
      "outputTemplate": { "response": "/#response", "body": "/#body" },
      "outputSchema": {
        "type": "object",
        "properties": { "response": { "type": "object" }, "body": { "type": "object" } },
        "additionalProperties": false
      },
      "sampleOutputContext": { "body": { "id": "rf_19", "status": "pending" } }
    }
  ],
  "i18n": {
    "en_us": {
      "acme_orders.settings.page.title": "Acme Orders",
      "acme_orders.settings.page.description": "Connect your Acme store.",
      "acme_orders.settings.path.default.apiKey.displayName": "API key",
      "acme_orders.settings.path.default.apiKey.description": "Create one in Acme under **Settings › API**.",
      "acme_orders.settings.path.default.storeDomain.displayName": "Store domain",
      "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.displayName": "Refund order",
      "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.description": "Issue a full or partial refund in Acme.",
      "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.properties.orderId.displayName": "Order ID",
      "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.properties.amount.displayName": "Amount"
    }
  },
  "changelog": {
    "1.4.0": "{{October 2026}} Added the Refund order action.",
    "1.3.0": "{{August 2026}} Orders show their tracking link."
  }
}
```

## Identity and listing

**Required**

**Listing**

**Platform**

## Components

Each of these keys installs something in the org. Component names follow the [naming scheme](https://developer.kustomer.com/docs/apps/register-and-install#names-are-namespaced) `kustomer.app.<app>.<name>`, and Kustomer applies it for you if you don't.

| Key                  | Installs                                                                                       | Guide                                                                                                                    |
| -------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `klasses`            | Custom object types, and attributes on customers, companies, conversations, messages and users | [Klasses](https://developer.kustomer.com/docs/apps/klasses)                                                              |
| `kviews`             | JSX views in the timeline and insights panel                                                   | [KViews](https://developer.kustomer.com/docs/apps/kviews)                                                                |
| `widgets`            | Iframes opened from the navigation rail                                                        | [Widgets](https://developer.kustomer.com/docs/apps/widgets)                                                              |
| `commands`           | Named server-side requests your UI can run                                                     | [KViews › Commands](https://developer.kustomer.com/docs/apps/kviews#call-apis-with-commands)                             |
| `hooks`              | Inbound webhook URLs that emit events                                                          | [Workflows, actions and triggers](https://developer.kustomer.com/docs/apps/workflows-actions-triggers#hooks-get-data-in) |
| `triggers`           | Named events with an output shape, for the workflow builder                                    | [Triggers](https://developer.kustomer.com/docs/apps/workflows-actions-triggers#triggers-name-the-event)                  |
| `actions`            | Workflow steps that call your API                                                              | [Actions](https://developer.kustomer.com/docs/apps/workflows-actions-triggers#actions-call-your-api)                     |
| `workflows`          | Complete automations                                                                           | [Workflows](https://developer.kustomer.com/docs/apps/workflows-actions-triggers#workflows-wire-it-together)              |
| `outboundWebhooks`   | Event subscriptions that post to your service                                                  | [Outbound webhooks](https://developer.kustomer.com/docs/apps/workflows-actions-triggers#outbound-webhooks)               |
| `settings`           | Per-org configuration and an auto-generated settings page                                      | [Settings](https://developer.kustomer.com/docs/apps/settings)                                                            |
| `settingsPageConfig` | Your own settings page, in an iframe                                                           | [Settings](https://developer.kustomer.com/docs/apps/settings#build-your-own-settings-page)                               |
| `auth`               | An OAuth 2.0 connection to a third-party API                                                   | [OAuth for apps](https://developer.kustomer.com/docs/apps/oauth#call-third-party-apis-with-oauth)                        |
| `i18n`               | Display strings for settings, widgets, actions and triggers                                    | [Internationalization](https://developer.kustomer.com/docs/apps/i18n)                                                    |
| `shortcuts`          | Saved replies that can also update the conversation                                            | [Shortcuts](#shortcuts)                                                                                                  |
| `templates`          | Starting points for reporting charts (`charts`) and business rules (`bizRules`)                | [Templates](#templates)                                                                                                  |

`cards` is deprecated; use a KView with `DynamicCard` instead.

## Shortcuts

A shortcut is a saved reply. Beyond inserting text, it can change the conversation when an agent uses it:

```json title="shortcuts (excerpt)"
{
  "shortcuts": [
    {
      "name": "acme-refund-issued",
      "draft": { "text": "Your refund is on its way. It can take 5 to 10 business days to appear." },
      "conversation": {
        "tags": { "operator": "append", "value": ["refund"] },
        "status": { "operator": "replace", "value": "done" }
      }
    }
  ]
}
```

`conversation` can set `tags`, `assignedToUsers` and `assignedToTeams` (`append`, `replace` or `remove`), `name`, `status` and `subStatus` (`replace` or `remove`), `snooze` and `queue` (`replace`), and `custom` attributes (`replace`). Limit who sees the shortcut with `accessTeams` and `accessUsers`.

## Templates

`templates.charts` and `templates.bizRules` add starting points to the reporting and business-rules builders, which admins use to build their own charts and rules. Business-rule templates hold the same `criteria` (with `and` and `or` groups of `<object>_<attribute>` conditions) and `actions` as a rule an admin builds. See [Criteria](https://developer.kustomer.com/docs/workflows/expressions-reference#criteria) for the condition format.

## Validation rules

Registration checks the whole definition before creating anything.

| Rule                                      | Error                                                                           |
| ----------------------------------------- | ------------------------------------------------------------------------------- |
| `app`, `version` and `title` are present  | `400`                                                                           |
| No unknown top-level keys                 | `400`                                                                           |
| `vendor` and `website` aren't used        | `400` "using deprecated fields! use appDetails…"                                |
| `app` isn't registered by another org     | `409` "The app name is already taken. Please choose a different name."          |
| `version` isn't already registered        | `409` "This version has already been registered. Please increment the version." |
| `version` is greater than the latest      | `400` "Cannot register outdated version. Version must be greater than …"        |
| Every component passes its own validation | `400` from the service that owns the component, such as a workflow schema error |

See [Register and install](https://developer.kustomer.com/docs/apps/register-and-install#troubleshooting) for fixes.

## Organize a large definition

Real apps have dozens of workflows and long JSX templates, which are painful to edit inside one JSON string. Keep each component in its own file and assemble the definition in a build step:

```text title="Project layout"
acme-orders/
├── app.json            # app, title, description, appDetails…
├── changelog.json
├── klasses/order.json
├── kviews/order-card.jsx
├── actions/refund.json
├── workflows/sync.json
├── i18n/en_us.json
└── build.mjs
```

```js title="build.mjs"
import { readFileSync, readdirSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'

const json = (p) => JSON.parse(readFileSync(p, 'utf8'))
const all = (dir) => readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => json(join(dir, f)))
const pkg = json('package.json')

const definition = {
  ...json('app.json'),
  version: pkg.version,
  changelog: json('changelog.json'),
  klasses: all('klasses'),
  kviews: [
    {
      name: 'kustomer.app.acme_orders.order-card',
      resource: 'kobject.acme_orders-order',
      context: 'smartbar-card',
      meta: { displayName: 'Acme order', icon: 'cart' },
      template: readFileSync('kviews/order-card.jsx', 'utf8'),
    },
  ],
  actions: all('actions'),
  workflows: all('workflows'),
  i18n: { en_us: json('i18n/en_us.json') },
}

writeFileSync('dist/acme_orders.json', JSON.stringify(definition, null, 2))
```

Taking `version` from `package.json` means `npm version minor` bumps the app and the build together.
