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.

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.

Built-in triggers

Event nameIn the builderFires when
kustomer.conversation.createConversation CreatedA conversation is created on any channel
kustomer.conversation.updateConversation UpdatedAny conversation attribute changes, including on every new message
kustomer.customer.createCustomer CreatedA customer is created
kustomer.customer.updateCustomer UpdatedA customer changes
kustomer.message.createMessage CreatedAn inbound or outbound message is created
kustomer.message.updateMessage UpdatedA message changes, for example its delivery status
kustomer.note.createNote CreatedAn agent or workflow adds a note
kustomer.note.updateNote UpdatedA note is edited
kustomer.company.createCompany CreatedA company is created
kustomer.company.updateCompany UpdatedA company changes
kustomer.kobject.<klass>.createKObject Created (klass)A custom object of that Klass is created, for example kustomer.kobject.order.create
kustomer.kobject.<klass>.updateKObject Updated (klass)A custom object of that Klass changes
kustomer.draft.createDraft CreatedAn agent starts a draft
kustomer.internal_thread.createInternal Thread CreatedAn internal collaboration thread starts
kustomer.internal_message.createInternal Message CreatedA message is posted in an internal thread

Two more families start workflows: inbound webhooks (kustomer.hooks.<type>.<id>.receive) and callable 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.

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.

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.

ActionIn the builderRequired inputs
kustomer.conversation.createConversation: Createcustomer
kustomer.conversation.updateConversation: Updateid
kustomer.conversation.find-by-idConversation: Lookup (By ID)id
kustomer.conversation.find-by-external-idConversation: Lookup (By External ID)externalId
kustomer.conversation.append-tagsConversation: Add Tagsid, tags
kustomer.conversation.remove-tagsConversation: Remove Tagsid, tags
kustomer.conversation.append-skillsConversation: Add Skillsid, skills
kustomer.conversation.remove-skillsConversation: Remove Skillsid, skills
kustomer.conversation.assign-userConversation: Assign Userid, user
kustomer.conversation.assign-teamConversation: Assign Teamid, team
kustomer.conversation.ai-agent-handoffAI Agent Handoffid, user

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

How Kustomer actions succeed and fail

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

ResponseWhat the step does
2xxSucceeds. The output is the record, flattened like trigger output
404Succeeds with empty output. A lookup that finds nothing isn't an error
Other 4xx, such as a validation errorFails. /#error is the first item of the API's errors array, and the step's error cases decide what happens next
5xxError 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:

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.

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.

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"] } }]
}
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.
InteractiveTest a regex step
flags
Hi, my order #4417 hasn't arrived
Full match Capture group
Step
{
  "id": "order-number",
  "action": "kustomer.regex-match.generic",
  "params": {
    "testString": "/#steps.1.preview",
    "regex": "order\\s*#?(\\d{4,})",
    "flags": "i"
  },
  "transitions": [
    {
      "target": "next-step",
      "condition": {
        "op": "exists",
        "values": [
          "/#steps.order-number.match"
        ]
      }
    }
  ]
}
In JSON, every backslash doubles
regex: "order\\s*#?(\\d{4,})"
Output
{
  "match": "order #4417",
  "captures": [
    "4417"
  ],
  "index": 7,
  "original": "Hi, my order #4417 hasn't arrived"
}
Use it in later steps
  • /#steps.order-number.match"order #4417"
  • /#steps.order-number.captures.0"4417"
  • /#steps.order-number.index7
Patterns run in your browser’s JavaScript engine, the same RegExp syntax the workflow step uses. Type the pattern as you’d write it in code; the step JSON shows the escaping it needs.

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.

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

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.typerunAt.value
relativeSeconds from now, such as 3600
absoluteAn 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.

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

MessageCause
runAt type must be one of absolute or relativerunAt.type is missing or misspelled
invalid runAt valueThe timestamp doesn't parse, or the relative value isn't whole seconds
Payload too large to schedule workflowinputs 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.