App definition anatomy

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

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.

InteractiveAnatomy of an app definition
{
}
kviews

KViews

JSX templates rendered inside Kustomer: insight cards, detail rows, and the expanded timeline view of your Klass. Embed your own hosted UI with DynamicCard and the Cards SDK.

Guide: KViews
Select a top-level key. The highlighted areas show where that part of the definition appears for admins and agents, or where it runs behind the scenes.

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.

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
appstringrequired
versionstringrequired
titlestringrequired
Listing
descriptionMarkdown
iconUrlURL
screenshotsURL[]
appDetailsobject
tagsstring[]
instructionsMarkdown
postInstallMessageMarkdown
releaseNotesUrlURL
changelogobject
Platform
identifierstring
visibilitystring
dependenciesstring[]
redirectUrisURL[]
metaobject

Components

Each of these keys installs something in the org. Component names follow the naming scheme kustomer.app.<app>.<name>, and Kustomer applies it for you if you don't.

KeyInstallsGuide
klassesCustom object types, and attributes on customers, companies, conversations, messages and usersKlasses
kviewsJSX views in the timeline and insights panelKViews
widgetsIframes opened from the navigation railWidgets
commandsNamed server-side requests your UI can runKViews › Commands
hooksInbound webhook URLs that emit eventsWorkflows, actions and triggers
triggersNamed events with an output shape, for the workflow builderTriggers
actionsWorkflow steps that call your APIActions
workflowsComplete automationsWorkflows
outboundWebhooksEvent subscriptions that post to your serviceOutbound webhooks
settingsPer-org configuration and an auto-generated settings pageSettings
settingsPageConfigYour own settings page, in an iframeSettings
authAn OAuth 2.0 connection to a third-party APIOAuth for apps
i18nDisplay strings for settings, widgets, actions and triggersInternationalization
shortcutsSaved replies that can also update the conversationShortcuts
templatesStarting points for reporting charts (charts) and business rules (bizRules)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:

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 for the condition format.

Validation rules

Registration checks the whole definition before creating anything.

RuleError
app, version and title are present400
No unknown top-level keys400
vendor and website aren't used400 "using deprecated fields! use appDetails…"
app isn't registered by another org409 "The app name is already taken. Please choose a different name."
version isn't already registered409 "This version has already been registered. Please increment the version."
version is greater than the latest400 "Cannot register outdated version. Version must be greater than …"
Every component passes its own validation400 from the service that owns the component, such as a workflow schema error

See Register and install 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:

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
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.