# Triggers and actions

> Every built-in workflow trigger and action, the shape of trigger output, how Kustomer actions report not-found and errors, and how to build callable and scheduled workflows.

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

A **trigger** names the event that starts a run. An **action** is a step that does something: finds or changes a record, calls a URL, or answers a caller. The built-in triggers and actions come from the Kustomer app, which every org has. Installed apps add their own. Use their names exactly as shown here in [workflow JSON](https://developer.kustomer.com/docs/workflows/workflow-json).

## Built-in triggers

| Event name                         | In the builder            | Fires when                                                                            |
| ---------------------------------- | ------------------------- | ------------------------------------------------------------------------------------- |
| `kustomer.conversation.create`     | Conversation Created      | A conversation is created on any channel                                              |
| `kustomer.conversation.update`     | Conversation Updated      | Any conversation attribute changes, including on every new message                    |
| `kustomer.customer.create`         | Customer Created          | A customer is created                                                                 |
| `kustomer.customer.update`         | Customer Updated          | A customer changes                                                                    |
| `kustomer.message.create`          | Message Created           | An inbound or outbound message is created                                             |
| `kustomer.message.update`          | Message Updated           | A message changes, for example its delivery status                                    |
| `kustomer.note.create`             | Note Created              | An agent or workflow adds a note                                                      |
| `kustomer.note.update`             | Note Updated              | A note is edited                                                                      |
| `kustomer.company.create`          | Company Created           | A company is created                                                                  |
| `kustomer.company.update`          | Company Updated           | A company changes                                                                     |
| `kustomer.kobject.<klass>.create`  | KObject Created (_klass_) | A custom object of that Klass is created, for example `kustomer.kobject.order.create` |
| `kustomer.kobject.<klass>.update`  | KObject Updated (_klass_) | A custom object of that Klass changes                                                 |
| `kustomer.draft.create`            | Draft Created             | An agent starts a draft                                                               |
| `kustomer.internal_thread.create`  | Internal Thread Created   | An internal collaboration thread starts                                               |
| `kustomer.internal_message.create` | Internal Message Created  | A message is posted in an internal thread                                             |

Two more families start workflows: [inbound webhooks](https://developer.kustomer.com/docs/events/inbound-webhooks) (`kustomer.hooks.<type>.<id>.receive`) and [callable workflows](#callable-and-scheduled-workflows) (`kustomer.workflow.<id>.call`). Installed apps define their own, such as `kustomer.app.<app>.<event>`.

### What the trigger outputs

A record trigger's output, step `1`, is the record **flattened**: attributes sit at the top level, relationships become plain IDs, and the update diff is under `changes`.

```json title="kustomer.conversation.update — step 1 output (abridged)"
{
  "id": "6512f0c2a4b1e1001a6b2c90",
  "name": "Order 4417 hasn't arrived",
  "status": "done",
  "priority": 3,
  "channels": ["email"],
  "tags": ["64f1c0ffee5ca1ab1e000001"],
  "assignedUsers": ["5f8481c6a22ab5b9d5f2dd99"],
  "assignedTeams": [],
  "messageCount": 4,
  "custom": { "orderNumberStr": "4417" },
  "lastDone": { "createdAt": "2026-10-21T15:20:42.554Z", "createdBy": "5f8481c6a22ab5b9d5f2dd99" },
  "customer": "6512ef18a4b1e1001a6b2c11",
  "brand": "5f8481c6a22ab5b9d5f2dd8b",
  "changes": {
    "attributes": {
      "status": { "op": "replace", "before": "open", "after": "done" }
    }
  }
}
```

So `/#steps.1.status` is the current status, `/#steps.1.customer` is the customer's ID (look the customer up if you need more), and `/#steps.1.changes.attributes.status.after` is set only when _this_ update changed the status. Create triggers have no `changes`.

Other trigger families shape step `1` differently:

- **Inbound web hooks** put the posted body under `attributes.data`, so a field named `email` is `/#steps.1.attributes.data.email`.
- **Callable workflows** get only the caller's inputs, cast to the trigger's `schema`. Read an input named `conversationId` as `/#steps.1.conversationId`, never `/#steps.1.inputs.conversationId`.

> **Use the builder's sample:** Every trigger and action in the builder has an output schema and sample data. When you select fields from the builder's dropdowns, it writes the correct `/#steps…` path for you. Copy the paths into JSON rather than guessing them.

## Built-in actions

The tables list each action's required inputs. Most actions accept many more optional ones, matching the fields of the record they write; the builder shows them all.

**Conversations**

| Action                                      | In the builder                        | Required inputs |
| ------------------------------------------- | ------------------------------------- | --------------- |
| `kustomer.conversation.create`              | Conversation: Create                  | `customer`      |
| `kustomer.conversation.update`              | Conversation: Update                  | `id`            |
| `kustomer.conversation.find-by-id`          | Conversation: Lookup (By ID)          | `id`            |
| `kustomer.conversation.find-by-external-id` | Conversation: Lookup (By External ID) | `externalId`    |
| `kustomer.conversation.append-tags`         | Conversation: Add Tags                | `id`, `tags`    |
| `kustomer.conversation.remove-tags`         | Conversation: Remove Tags             | `id`, `tags`    |
| `kustomer.conversation.append-skills`       | Conversation: Add Skills              | `id`, `skills`  |
| `kustomer.conversation.remove-skills`       | Conversation: Remove Skills           | `id`, `skills`  |
| `kustomer.conversation.assign-user`         | Conversation: Assign User             | `id`, `user`    |
| `kustomer.conversation.assign-team`         | Conversation: Assign Team             | `id`, `team`    |
| `kustomer.conversation.ai-agent-handoff`    | AI Agent Handoff                      | `id`, `user`    |

Use `append-tags` and `remove-tags` rather than `update` with `tags`: `update` replaces the whole array, so a concurrent change can be lost.

**Customers**

| Action                                   | In the builder                                   | Required inputs         |
| ---------------------------------------- | ------------------------------------------------ | ----------------------- |
| `kustomer.customer.create`               | Customer: Create                                 | none                    |
| `kustomer.customer.update`               | Customer: Update                                 | `id`                    |
| `kustomer.customer.find`                 | Customer: Lookup (By ID)                         | `identifier`            |
| `kustomer.customer.find-by-email`        | Customer: Lookup (By Email)                      | `email`                 |
| `kustomer.customer.find-by-phone`        | Customer: Lookup (By Phone)                      | `phone`                 |
| `kustomer.customer.find-by-external-id`  | Customer: Lookup (By External ID)                | `externalId`            |
| `kustomer.customer.find-by-whatsapp`     | Customer: Lookup (By WhatsApp Phone)             | `whatsapp`              |
| `kustomer.customer.find-by-bsuid`        | Customer: Lookup (By WhatsApp BSUID)             | `bsuid`, `bsuidScopeId` |
| `kustomer.customer.find-by-facebook-id`  | Customer: Lookup (By Facebook Messenger User ID) | `pageId`, `userId`      |
| `kustomer.customer.find-by-instagram-id` | Customer: Lookup (By Instagram Thread ID)        | `threadId`              |
| `kustomer.customer.append-email`         | Customer: Append Email                           | `id`, `email`           |
| `kustomer.customer.append-phone`         | Customer: Append Phone                           | `id`, `phone`           |
| `kustomer.customer.append-external-id`   | Customer: Append External ID                     | `id`, `externalId`      |
| `kustomer.customer.merge`                | Merge Customers                                  | `source`, `target`      |
| `kustomer.message.find-last`             | Customer: Lookup Last Message                    | `customer`              |

**Messages and notes**

| Action                              | In the builder                   | Required inputs                      |
| ----------------------------------- | -------------------------------- | ------------------------------------ |
| `kustomer.message.create`           | Message: Create                  | `customer`, `channel`, `app`, `size` |
| `kustomer.message.update`           | Message: Update                  | `id`                                 |
| `kustomer.message.find-by-id`       | Message: Lookup (By ID)          | `id`                                 |
| `kustomer.message.find-by-external` | Message: Lookup (By External ID) | `identifier`                         |
| `kustomer.note.create`              | Note: Create                     | `conversation`, `body`               |
| `kustomer.note.update`              | Note: Update                     | `id`                                 |
| `kustomer.note.find-by-id`          | Note: Lookup (By ID)             | `id`                                 |
| `kustomer.note.find-by-external`    | Note: Lookup (By External ID)    | `identifier`                         |

`message.create` records a message on a conversation. It doesn't send anything to the customer. To reply on a channel, see [Conversations and messages](https://developer.kustomer.com/docs/concepts/conversations-and-messages).

**Custom objects and companies**

| Action                                  | In the builder                                | Required inputs                          |
| --------------------------------------- | --------------------------------------------- | ---------------------------------------- |
| `kustomer.kobject.create`               | Custom Object (KObject): Create               | `klassName`, `title`, `data`             |
| `kustomer.kobject.create-with-customer` | Custom Object (KObject): Create with Customer | `klassName`, `customer`, `title`, `data` |
| `kustomer.kobject.update`               | Custom Object (KObject): Update               | `id`, `klassName`, `title`, `data`       |
| `kustomer.kobject.find`                 | Custom Object (KObject): Lookup (By ID)       | `id`, `klassName`                        |
| `kustomer.kobject.delete`               | Custom Object (KObject): Delete               | `id`, `klassName`                        |
| `kustomer.klass.create`                 | Klass: Create                                 | `name`, `status`                         |
| `kustomer.klass.find-by-klassname`      | Klass: Lookup (By Klass name)                 | `klassName`                              |
| `kustomer.company.create`               | Company: Create                               | `name`                                   |
| `kustomer.company.update`               | Company: Update                               | `id`                                     |
| `kustomer.company.find`                 | Company: Lookup (By ID)                       | `identifier`                             |
| `kustomer.company.find-by-external-id`  | Company: Lookup (By External ID)              | `externalId`                             |

**Users and collaboration**

| Action                                   | In the builder                   | Required inputs                                                         |
| ---------------------------------------- | -------------------------------- | ----------------------------------------------------------------------- |
| `kustomer.user.find-by-id`               | User: Find By ID                 | `id`                                                                    |
| `kustomer.user.find-by-email`            | User: Find By Email              | `email`                                                                 |
| `kustomer.internal_thread.create`        | Internal thread: Create          | `conversation`, `title`, `internalChannel`                              |
| `kustomer.draft.internal-message-create` | Draft: Internal message          | `customer`, `conversation`                                              |
| `kustomer.internal_message.create`       | Internal message: Create (draft) | `draftId`, `body`, `internalThreadId`, `threadTitle`, `internalChannel` |

**Utilities**

| Action                                     | In the builder                      | Required inputs                 |
| ------------------------------------------ | ----------------------------------- | ------------------------------- |
| `kustomer.rest-api.json`                   | Rest API: JSON                      | `uri`, `method`                 |
| `kustomer.rest-api.generic`                | Rest API: Generic                   | `uri`, `method`                 |
| `kustomer.regex-match.generic`             | Generic Regex Match                 | `testString`, `regex`           |
| `kustomer.workflow.respond`                | Workflow: Send Response             | `outputs`                       |
| `kustomer.app.scheduler.schedule-workflow` | Schedule a Workflow (Scheduler app) | `workflowId`, `runAt`, `inputs` |

## How Kustomer actions succeed and fail

Kustomer actions call the same APIs you do, and the step's outcome follows the response:

| Response                                | What the step does                                                                                                                                                                                            |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `2xx`                                   | Succeeds. The output is the record, flattened like trigger output                                                                                                                                             |
| `404`                                   | **Succeeds with empty output.** A lookup that finds nothing isn't an error                                                                                                                                    |
| Other `4xx`, such as a validation error | Fails. `/#error` is the first item of the API's `errors` array, and the step's [error cases](https://developer.kustomer.com/docs/workflows/conditions-and-expressions#handle-errors) decide what happens next |
| `5xx`                                   | **Error cases are skipped and the run starts again from step `1`.** Steps that already succeeded run again. These retries aren't recorded on the workflow's error log                                         |

Always supply required inputs. The request is sent even when a required input is missing, so a step with a missing or empty required input may fail or return empty output. `0`, `false` and `""` count as missing for this check. A param whose reference resolves to `null`, `undefined` or an empty object isn't sent at all. The one exception is `queue`, where `null` is kept and means "unassign".

Because a `5xx` reruns the whole workflow, design steps before a risky one to be safe to repeat: prefer lookups and **Add Tags** over creating records, or look for the record before you create it.

Because not-found isn't a failure, branch on the result after every lookup. This is the pattern Kustomer's own apps use:

```json title="Find or create"
{
  "id": "find",
  "action": "kustomer.customer.find-by-email",
  "params": { "email": "/#steps.1.attributes.data.email" },
  "transitions": [
    { "target": "update", "condition": { "op": "exists", "values": ["/#steps.find.id", ""] }, "meta": { "name": "Customer exists" } },
    { "target": "create", "condition": { "op": "dne", "values": ["/#steps.find.id", ""] }, "meta": { "name": "New customer" } }
  ]
}
```

The REST actions behave differently: they never fail on a status code. See [Call your own code](https://developer.kustomer.com/docs/workflows/code-steps#handle-responses-and-failures).

## Utility actions

### REST API

`kustomer.rest-api.json` sends `data` as JSON and, with `"json": true`, parses the response body into an object. `kustomer.rest-api.generic` sends `data` as a string, for example form-encoded or XML, and returns the body as a string. Both output `{ response: { statusCode, headers }, body }`. The full guide, including signing, secrets and URL restrictions, is in [Call your own code](https://developer.kustomer.com/docs/workflows/code-steps).

### Regex match

Extracts values from text such as an email subject or message body.

```json
{
  "id": "order-number",
  "action": "kustomer.regex-match.generic",
  "params": { "testString": "/#steps.1.preview", "regex": "order\\s*#?(\\d{4,})", "flags": "i" },
  "transitions": [{ "target": "lookup-order", "condition": { "op": "exists", "values": ["/#steps.order-number.match"] } }]
}
```

```json title="Output"
{ "match": "order #4417", "captures": ["4417"], "index": 7, "original": "Hi, my order #4417 hasn't arrived" }
```

Reference the first capture group as `/#steps.order-number.captures.0`. Without a match, the output has only an empty `captures`, so check `exists` on `match` before using it.

The step runs JavaScript's `RegExp` and `String.match()`:

- **Pass flags in the `flags` param.** Inline flags such as `(?i)` are a syntax error. An invalid pattern fails the step with the engine's error, such as `Invalid regular expression: /(?i)refund/: Invalid group`.
- **Leave out `g` to get capture groups.** With `g`, `match` is the first full match, `captures` holds the other full matches, and `index` and `original` are missing.
- **Escape backslashes in JSON**: `\d` is written `"\\d"`.
- If `regex` or `testString` resolves to nothing, the step fails with `Missing param regex` or `Missing param testString`.

_A regex step tester. Type a pattern, toggle the i, g, m, s, u and y flags, and edit the sample text: the full match and capture groups are highlighted in the text. Beside it, the step JSON updates with the pattern escaped for JSON, each doubled backslash highlighted. The output panel shows the step's match, captures, index and original, and the references later steps use, such as /#steps.order-number.captures.0. Examples load an order number, keywords, an email address, the g flag (captures become the other matches) and an inline (?i) flag (the step fails with the engine's syntax error). Notes explain no match, case sensitivity, missing capture groups and the g flag._

## Callable and scheduled workflows

A **callable** workflow has no event trigger. Instead, its trigger declares the inputs it accepts, and something else calls it, most often the Scheduler app's **Schedule a Workflow** action.

```json title="Callable workflow: send-followup"
{
  "name": "send-followup",
  "enabled": true,
  "trigger": {
    "id": "1",
    "callable": true,
    "schema": {
      "type": "object",
      "properties": { "conversationId": { "type": "string" }, "reason": { "type": "string" } },
      "required": ["conversationId"]
    },
    "transitions": [{ "target": "load", "condition": { "op": "exists", "values": ["/#steps.1.conversationId"] } }]
  },
  "response": {
    "schema": { "properties": { "noteId": { "type": "string" } }, "required": [] }
  },
  "steps": [
    {
      "id": "load",
      "action": "kustomer.conversation.find-by-id",
      "params": { "id": "/#steps.1.conversationId" },
      "transitions": [{ "target": "note", "condition": { "op": "neq", "values": ["/#steps.load.status", "done"] } }]
    },
    {
      "id": "note",
      "action": "kustomer.note.create",
      "params": { "conversation": "/#steps.load.id", "body": "Follow up: {{steps.1.reason}}" },
      "transitions": [{ "target": "respond", "condition": { "op": "true", "values": [true] } }]
    },
    {
      "id": "respond",
      "action": "kustomer.workflow.respond",
      "params": { "outputs": { "noteId": "/#steps.note.id" } },
      "transitions": []
    }
  ]
}
```

- Don't set `eventName`. Kustomer sets it to `kustomer.workflow.<workflowId>.call` when you save.
- A callable trigger needs `id`, `callable`, `schema` and `transitions`. In the builder, the **Input Parameters** tab of **Edit Input Parameters** writes `trigger.schema`.
- The caller's inputs are cast to `schema` and become step `1`'s output, and nothing else is added, so `conversationId` is `/#steps.1.conversationId`.
- The root `response.schema` declares the outputs the workflow sends back. It's the builder's **Output Parameters** tab.
- `kustomer.workflow.respond` returns `outputs` to a caller that waits for a result, such as a Kustomer AI agent that calls the workflow as a tool. Add it at the end of each branch that should answer. The caller gets the `outputs` on success, "no response" if the run ends without reaching a respond step, or an error result if the run fails.
- Calls to callable workflows aren't rate limited.
- There's no public REST endpoint that calls a workflow directly. To run one from your own code, post to an [inbound web hook](https://developer.kustomer.com/docs/events/inbound-webhooks) and use a workflow triggered by it.

### Schedule a workflow

The Scheduler app's action runs a callable workflow later. Re-check the state in the callable workflow, as `load` does above, because it might have changed by the time it runs.

```json title="In the caller: run send-followup in 24 hours"
{
  "id": "schedule",
  "action": "kustomer.app.scheduler.schedule-workflow",
  "appVersion": "scheduler-^1.0.1",
  "params": {
    "workflowId": "6513a0d1a4b1e1001a6b3e01",
    "runAt": { "type": "relative", "value": 86400 },
    "inputs": { "conversationId": "/#steps.1.id", "reason": "No reply after 24 hours" }
  },
  "transitions": []
}
```

| `runAt.type` | `runAt.value`                                                                                   |
| ------------ | ----------------------------------------------------------------------------------------------- |
| `relative`   | Seconds from now, such as `3600`                                                                |
| `absolute`   | An ISO 8601 timestamp, such as `"2026-10-22T14:00:00Z"`, or one computed with the `date` helper |

`workflowId`, `runAt` and `inputs` are all required; pass `{}` when there are no inputs. A relative `value` must be a whole number of seconds. When the time comes, the callable workflow runs with those `inputs` as its step `1` output. To keep a scheduled reminder within working hours, compute an absolute time first, or check `/#fn:inBusinessHours` in the callable workflow. See the [Expressions reference](https://developer.kustomer.com/docs/workflows/expressions-reference).

> **The date helper counts backwards:** `{{{date 'hours' count=24}}}` is 24 hours **ago**, so it schedules a run in the past. For a time in the future, use a negative count, `{{{date 'hours' count=-24}}}`, or skip the helper and use `"runAt": { "type": "relative", "value": 86400 }`.

The step fails with one of these messages when its inputs are wrong:

| Message                                          | Cause                                                                          |
| ------------------------------------------------ | ------------------------------------------------------------------------------ |
| `runAt type must be one of absolute or relative` | `runAt.type` is missing or misspelled                                          |
| `invalid runAt value`                            | The timestamp doesn't parse, or the relative value isn't whole seconds         |
| `Payload too large to schedule workflow`         | `inputs` is too big. Pass IDs and look the records up in the callable workflow |

## Actions and triggers from apps

Apps add triggers and actions with their own names, such as `kustomer.app.<app>.<event>`. A step that uses an app's action includes `appVersion`, such as `"scheduler-^1.0.1"`, so the workflow keeps working as the app updates within that major version. See [Apps: workflows, actions and triggers](https://developer.kustomer.com/docs/apps/workflows-actions-triggers).
