# Workflows, actions and triggers

> Bring data into Kustomer with app hooks, name it with triggers, call your API with REST actions, ship ready-made workflows, and subscribe to events with outbound webhooks.

Source: https://developer.kustomer.com/docs/apps/workflows-actions-triggers

Most integrations follow one loop: your platform tells Kustomer something happened, a workflow decides what to do, and Kustomer calls your platform back. An app packages each piece so it's installed and configured the same way in every org.

```mermaid
flowchart LR
  P[Your platform] -- POST --> H[hooks<br/>per-install URL]
  H -- event --> T[triggers<br/>name + output shape]
  T --> W[workflows]
  W --> K[Kustomer actions<br/>create, update, find]
  W --> A[actions<br/>your REST API]
  A -- HTTPS --> P
  E[(Kustomer events)] -- outboundWebhooks --> P
```

| Key                | Does                                              | Runs                                      |
| ------------------ | ------------------------------------------------- | ----------------------------------------- |
| `hooks`            | Creates an inbound webhook URL in each org        | When your platform posts to it            |
| `triggers`         | Describes a hook's event for the workflow builder | At the start of each workflow run         |
| `actions`          | Packages a call to your API as a workflow step    | Inside workflows, yours and the org's own |
| `workflows`        | Installs complete automations                     | On their trigger                          |
| `outboundWebhooks` | Subscribes your URL to Kustomer events            | On every matching event                   |

This page covers what's specific to apps. For how workflows run, their JSON format, conditions and expressions, see the [Workflows](https://developer.kustomer.com/docs/workflows) section.

## Hooks: get data in

```json title="hooks (excerpt)"
{
  "hooks": [
    {
      "eventName": "kustomer.app.acme_orders.order-updated",
      "displayEventName": "Acme order updated",
      "description": "Acme posts the full order whenever it changes.",
      "type": "web"
    }
  ]
}
```

**Hook**

Each install gets its own hook URL, which admins find on the app's page in the App Directory. Your platform needs that URL, so either ask admins to paste it into your platform (say how in `postInstallMessage`), or have a [custom settings page](https://developer.kustomer.com/docs/apps/settings#build-your-own-settings-page) read it and register it with your platform for them.

For request formats, size limits and debugging, see [Inbound webhooks](https://developer.kustomer.com/docs/events/inbound-webhooks).

> **Form hooks are public:** Anyone with a form hook's URL can post to it. Use `web` hooks for server-to-server data, or verify a shared secret in your workflow's first condition.

## Triggers: name the event

A trigger tells the workflow builder about a hook's event: what to call it, and what data a workflow receives.

```json title="triggers (excerpt)"
{
  "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",
        "total": "/#data.attributes.data.total"
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "orderId": { "type": "string" },
          "email": { "type": "string" },
          "status": { "type": "string" },
          "total": { "type": "number" }
        },
        "additionalProperties": false
      },
      "sampleOutputContext": { "orderId": "4417", "email": "ada@example.com", "status": "shipped", "total": 86.5 }
    }
  ]
}
```

**Trigger**

With both `outputTemplate` and `outputSchema`, a workflow reads the shaped values as `/#steps.1.orderId`. Without them, step 1 is the raw hook record and the posted body is at `/#steps.1.attributes.data.<field>`. The shaped form is easier for admins building their own workflows, and casting means `"86.50"` arrives as the number `86.5`.

Name the trigger in the builder with [i18n](https://developer.kustomer.com/docs/apps/i18n#workflow-builder-strings) keys.

## Actions: call your API

An action is a reusable workflow step with typed inputs. Third-party apps use the `rest_api` type, which makes an HTTP request:

```json title="actions (excerpt)"
{
  "actions": [
    {
      "name": "kustomer.app.acme_orders.refund",
      "description": "Refund an Acme order",
      "type": "rest_api",
      "appSettings": {
        "apiKey": { "key": "acme_orders.default.apiKey" },
        "storeDomain": { "key": "acme_orders.default.storeDomain" }
      },
      "inputTemplate": {
        "uri": "https://{{storeDomain}}/api/orders/{{orderId}}/refunds",
        "method": "POST",
        "headers": { "Authorization": "Bearer {{apiKey}}", "Content-Type": "application/json" },
        "data": { "amount": "/#amount", "reason": "/#reason" },
        "json": true
      },
      "inputSchema": {
        "type": "object",
        "properties": {
          "orderId": { "type": "string" },
          "amount": { "type": "number" },
          "reason": { "type": "string" }
        },
        "required": ["orderId"],
        "additionalProperties": false
      },
      "outputTemplate": { "response": "/#response", "body": "/#body" },
      "outputSchema": {
        "type": "object",
        "properties": { "response": { "type": "object" }, "body": { "type": "object" } },
        "additionalProperties": false
      },
      "sampleOutputContext": { "response": { "statusCode": 201 }, "body": { "id": "rf_19", "status": "pending" } }
    }
  ]
}
```

**Action**

Inside `inputTemplate`, `{{name}}` in strings inserts an input or setting, and a whole-value `"/#amount"` inserts the input with its type intact. Set `json: true` to parse the response body as JSON; otherwise `body` is a string.

> **Same rules as REST steps:** App actions behave like the workflow REST step: non-2xx responses don't fail the step, so check `/#steps.<id>.response.statusCode` in the next transition. See [Call your own code](https://developer.kustomer.com/docs/workflows/code-steps#handle-responses-and-failures) for status handling, timeouts and signing requests.

A workflow step that uses the action names it and the app version range:

```json title="Workflow step"
{
  "id": "refund",
  "action": "kustomer.app.acme_orders.refund",
  "appVersion": "acme_orders-^1.4.0",
  "params": { "orderId": "/#steps.1.orderId", "amount": "/#steps.1.total", "reason": "Requested in chat" },
  "transitions": []
}
```

Give the action, and each input, a display name with [i18n](https://developer.kustomer.com/docs/apps/i18n#workflow-builder-strings). Without one, admins see the raw action name.

## Workflows: wire it together

App workflows use the same [workflow JSON](https://developer.kustomer.com/docs/workflows/workflow-json) as any other, with two differences: names must be unique across every workflow in the org, and steps that use app actions carry `appVersion`.

```json title="workflows (excerpt)"
{
  "workflows": [
    {
      "name": "acme-orders-sync",
      "description": "Create or update an Acme order KObject when Acme sends an update.",
      "trigger": {
        "eventName": "kustomer.app.acme_orders.order-updated",
        "appVersion": "acme_orders-^1.4.0",
        "transitions": [{ "target": "findCustomer", "condition": { "op": "true", "values": [true] } }]
      },
      "steps": [
        {
          "id": "findCustomer",
          "action": "kustomer.customer.find-by-email",
          "appVersion": "kustomer-^1.9.0",
          "params": { "email": "/#steps.1.email" },
          "transitions": [
            { "target": "findOrder", "condition": { "op": "exists", "values": ["/#steps.findCustomer.id"] } }
          ]
        },
        {
          "id": "findOrder",
          "action": "kustomer.kobject.find-by-external-id",
          "appVersion": "kustomer-^1.9.0",
          "params": { "klassName": "acme_orders-order", "externalId": "/#steps.1.orderId" },
          "transitions": [
            { "target": "updateOrder", "condition": { "op": "exists", "values": ["/#steps.findOrder.id"] } },
            { "target": "createOrder", "condition": { "op": "true", "values": [true] } }
          ]
        },
        {
          "id": "updateOrder",
          "action": "kustomer.kobject.update",
          "appVersion": "kustomer-^1.9.0",
          "params": {
            "id": "/#steps.findOrder.id",
            "klassName": "acme_orders-order",
            "title": "Order #{{steps.1.orderId}}",
            "data": {},
            "custom": { "statusStr": "/#steps.1.status", "totalNum": "/#steps.1.total" }
          },
          "transitions": []
        },
        {
          "id": "createOrder",
          "action": "kustomer.kobject.create-with-customer",
          "appVersion": "kustomer-^1.9.0",
          "params": {
            "customer": "/#steps.findCustomer.id",
            "klassName": "acme_orders-order",
            "externalId": "/#steps.1.orderId",
            "title": "Order #{{steps.1.orderId}}",
            "data": {},
            "custom": { "orderNumberStr": "/#steps.1.orderId", "statusStr": "/#steps.1.status", "totalNum": "/#steps.1.total" }
          },
          "transitions": []
        }
      ]
    }
  ]
}
```

Because a Kustomer action that finds nothing succeeds with an empty output, the `exists` checks route each run down the right branch. See [How Kustomer actions succeed and fail](https://developer.kustomer.com/docs/workflows/triggers-and-actions#how-kustomer-actions-succeed-and-fail).

### Read settings in workflows

Use the `setting` helper in any template to read the installing org's values:

```json
{ "params": { "uri": "https://{{{ setting acme_orders.default.storeDomain }}}/api/orders" } }
```

Keep secrets out of workflow templates: workflow JSON and run logs are visible to admins. Reference secret settings from an action's `appSettings` instead, where they're filled in at request time.

### Let admins customize your workflows

Admins often want to tweak an app workflow. Rather than editing it (an update to your app would overwrite their change), they copy it and set `replaces` on the copy to your workflow's name. While their copy is enabled, yours is skipped. See [Override an app workflow](https://developer.kustomer.com/docs/workflows/workflow-json#override-an-app-workflow-with-replaces).

### Build workflows in the editor first

Writing large workflows by hand is slow. A faster loop:

#### Register a version with your actions and triggers

Ship `hooks`, `triggers`, `actions` and `settings`, without workflows.

#### Build in the workflow builder

Install it in a sandbox and build the workflow visually, using your app's actions and trigger.

#### Export the JSON

Read it with `kusty api GET /v1/workflows/<id> --output json`, remove the read-only attributes, and replace hard-coded values with `setting` helpers.

#### Ship it

Add it to `workflows`, give it a name unique to your app, bump the version and register.

## Outbound webhooks

`outboundWebhooks` subscribes your service to Kustomer events in every org that installs the app, with no setup by the admin:

```json title="outboundWebhooks (excerpt)"
{
  "outboundWebhooks": [
    {
      "name": "Acme order sync",
      "url": "https://hooks.acme.example/kustomer",
      "events": ["kustomer.conversation.create", "kustomer.conversation.update"],
      "headers": [{ "name": "x-acme-source", "value": "kustomer" }]
    }
  ]
}
```

**Outbound webhook**

Every org posts to the same `url`. Use the `org` field in each payload to tell them apart. Payloads, signatures, retries and delivery history work the same as for any [outbound webhook](https://developer.kustomer.com/docs/events/outbound-webhooks).

> **Public apps:** Kustomer reviews outbound webhooks closely before publishing an app: subscribe only to the events the app needs, and describe the data you receive in the app's `description`.
