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.

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.

POST

event

HTTPS

outboundWebhooks

Your platform

hooks
per-install URL

triggers
name + output shape

workflows

Kustomer actions
create, update, find

actions
your REST API

Kustomer events

Diagram source
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
KeyDoesRuns
hooksCreates an inbound webhook URL in each orgWhen your platform posts to it
triggersDescribes a hook's event for the workflow builderAt the start of each workflow run
actionsPackages a call to your API as a workflow stepInside workflows, yours and the org's own
workflowsInstalls complete automationsOn their trigger
outboundWebhooksSubscribes your URL to Kustomer eventsOn every matching event

This page covers what's specific to apps. For how workflows run, their JSON format, conditions and expressions, see the Workflows section.

Hooks: get data in

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
eventNamestringrequired
displayEventNamestring
descriptionstring
typestringrequired

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 read it and register it with your platform for them.

For request formats, size limits and debugging, see Inbound webhooks.

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.

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
eventNamestringrequired
descriptionstring
outputTemplateobject
outputSchemaJSON Schema
sampleOutputContextobject

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

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
namestringrequired
typestringrequired
descriptionstring
appSettingsobject
appAccountNamestring
inputTemplateobject
inputSchemaJSON Schema
outputTemplateobject
outputSchemaJSON Schema
sampleOutputContextobject

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.

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

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. Without one, admins see the raw action name.

Workflows: wire it together

App workflows use the same 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.

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.

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.

Build workflows in the editor first

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

  1. Register a version with your actions and triggers

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

  2. Build in the workflow builder

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

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

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

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
namestringrequired
urlURLrequired
eventsstring[]required
headersobject[]

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.