> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ilyama.golain.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Automation: triggers, due dates and actions

> Open tickets from platform events and alarms, run SLA timers with due policies, call webhooks and device RPCs on transitions, and install workflow templates.

Workflow **automation** lets the platform act on tickets without anyone clicking through the Console. You declare all of it in the workflow spec, alongside labels and transitions:

| You want to… | Use | Where |
| - | - | - |
| Open a ticket when a device, OTA or calendar event happens | `trigger_intake` | Workflow spec |
| Open a work order when an alarm fires | **Work order on activation** | Alarm definition |
| Warn before a deadline, or move overdue tickets | `due_policy` + ticket `due_at` | Workflow spec + ticket |
| Call an HTTP endpoint or a device method on a state change | `on_enter` / `on_exit` actions | Workflow transitions |
| Start from a ready-made work-order workflow | Workflow templates | Templates API |

Automation only runs for the **active** workflow version. After you create or edit a workflow, activate the new version. See [Workflows](/ticketing/workflows.md) for the authoring loop.

> **[Workflows](/ticketing/workflows.md)**
>
> Labels, transitions, permits, guards, validation and activation.

> **[Assignment and notifications](/ticketing/assignment-notifications.md)**
>
> Subscribe to **Ticket Due Soon**, **Ticket Due** and other ticket events.

> **[API reference](/ticketing/api-reference.md)**
>
> Every ticketing route, request body and response field.

> **[Troubleshooting](/ticketing/troubleshooting.md)**
>
> Intake that never fires, timers that do not move, failed actions.

> [!NOTE]
>
> Examples on this page are YAML for readability. The create and patch workflow endpoints take the same spec as JSON under a `workflow` key: `{"workflow": {...}}`. `POST …/ticket-workflows/validate` accepts either that JSON body or the raw YAML.

## Trigger intake

**Trigger intake** opens a ticket automatically when a matching platform event arrives in the project. Each `trigger_intake` entry names one event and uses CEL expressions to decide whether to open a ticket and what to put in it.

```mermaid theme={null}
flowchart LR
  E[Platform event] --> F{filter}
  F -->|false| X[Ignored]
  F -->|true| G{group_by set?}
  G -->|yes| B[Group buffer] -->|window closes| T[One ticket]
  G -->|no| D{dedupe bucket in cooldown?}
  D -->|yes| X
  D -->|no| T2[New ticket in initial label]
```

Tickets opened this way:

* Start in the workflow's `initial` label, on the active workflow version.
* Have no creator (`created_by` is empty) and no assignee. Assign them afterwards, or let a person pick them up.
* Have no `due_at`. Patch one in if the ticket needs a deadline (see [Due dates and due policy](#due-dates-and-due-policy)).

### Spec fields

| Field | Required | Description |
| - | - | - |
| `event` | Yes | Event key from [Supported events](#supported-events). Each event can appear at most once per workflow. |
| `filter` | No | CEL expression returning `bool`. When it returns `false`, the event is ignored. |
| `title` | Yes | CEL expression returning a non-empty string. Becomes the ticket title. |
| `dedupe_key` | No | CEL expression returning a string. Names the dedup bucket. See [Deduplication](#deduplication). |
| `cooldown` | No | Duration such as `30s`, `15m`, `24h` or `720h`. Empty or `0s` turns dedup off. Minimum `1s` when set. |
| `tags` | No | List of `{ kind, value }`. `kind` is a literal tag kind; `value` is a CEL expression returning a string. |
| `metadata` | No | Map of metadata key to CEL expression. Results are written into the ticket's `metadata`. Keys must not contain dots or whitespace. |
| `group_by` | No | List of CEL expressions returning strings. Together they form the group key. Turns on [Grouping](#grouping). |
| `group_wait` | No | How long to collect events for a group before opening its ticket, such as `5m`. `0s` or empty opens the ticket on the first event. Minimum `1s` when set. |
| `group_interval` | No | Quiet period after a group opens a ticket. Events for the same group key are dropped until it ends. `0s` or empty means no quiet period. Minimum `1s` when set. |
| `group_wait_mode` | No | `fixed` (default) or `debounce`. |

Durations use the units `ms`, `s`, `m` and `h`. There is no day unit, so write `24h` or `720h`.

> [!WARNING]
>
> A workflow with any `trigger_intake` entry must declare its `initial` label under `labels.extra` with `class: open`. Validation rejects the workflow otherwise.

### CEL variables

| Variable | Contents |
| - | - |
| `event.key` | The event key, for example `device.device.updated.v1` |
| `event.payload` | The event's JSON payload as a map, for example `event.payload.device_id` |
| `project.id` | The project UUID |
| `group.event_count` | Number of events collected in the group (`1` when grouping is off) |
| `group.first_fired_at` | RFC3339 time of the first event in the group (empty when grouping is off) |
| `group.key` | The evaluated group key (empty when grouping is off) |

The standard CEL string extensions are available, for example `lowerAscii()` and `split()`.

Some payload fields, such as `fleet_id` and `device_name`, are left out when empty. Reading a missing field is an error, and an error means no ticket. Guard optional fields with `has()`, for example `has(event.payload.fleet_id) ? event.payload.fleet_id : "none"`.

### Supported events

Validation accepts only these events in `trigger_intake`:

| Event | Fires when | Useful payload fields |
| - | - | - |
| `device.device.created.v1` | A device is registered | `device_id`, `fleet_id`, `project_id`, `device_name`, `fleet_name` |
| `device.device.updated.v1` | A device's details change | Same as above |
| `device.device.deleted.v1` | A device is deleted | Same as above |
| `device.ota_subscription.subscribed.v1` | A device subscribes to its OTA topic | `device_id`, `topic`, `project_id` |
| `ota.deployment.triggered.v1` | An OTA deployment starts | `deployment_id`, `release_id`, `project_id` |
| `ota.deployment.failed.v1` | An OTA deployment fails on one device | `deployment_id`, `device_id`, `reason`, `attempts` |
| `calendar.occurrence.started.v1` | A calendar occurrence starts | `event_id`, `schedule_id`, `occurrence_at`, `kind_key`, `asset_ids` |
| `calendar.occurrence.ended.v1` | A calendar occurrence ends | Same as above. Moves a ticket instead of opening one (see below). |
| `telemetry.data_point.persisted.v1` | — | See warning |
| `telemetry.dataspace.persisted.v1` | — | See warning |

> [!WARNING]
>
> The two `telemetry.*` events pass validation, but the platform does not currently publish them. An intake bound to them never opens a ticket. For value thresholds on device data, use an [alarm](/alarms/overview.md) with **Work order on activation** instead.

**Calendar events** behave differently from the others:

* `calendar.occurrence.started.v1` dedupes per occurrence by default. If you leave `dedupe_key` empty, the platform uses the schedule and occurrence time as the key, so a redelivered event cannot open a second ticket for the same occurrence. The new ticket is linked back to the calendar event.
* `calendar.occurrence.ended.v1` does not open a ticket. It finds the ticket opened for that occurrence and moves it along the **first** transition declared from its current label. The move runs as the system actor, so that transition must include `system` in its `permit` list.

### Deduplication

Set `dedupe_key` and a positive `cooldown` to stop bursts of events from opening a ticket each.

* **Bucket.** A bucket is the workflow version, plus the event, plus the evaluated `dedupe_key`. Two workflows listening to the same event dedupe independently. An empty `dedupe_key` puts every event for that intake in one shared bucket.
* **Window.** The first event in a bucket opens a ticket and starts the cooldown. Later events in the same bucket are dropped until the cooldown ends.
* **Time-based only.** Closing the ticket does not reset the window, and an open ticket does not extend it. There is no "one open ticket per device" mode. For "at most one ticket per device per day", use a per-device `dedupe_key` with `cooldown: 24h`.
* **Off.** Empty or `0s` cooldown means every event that passes `filter` opens a ticket.
* **New version, new buckets.** Activating a new workflow version starts with empty buckets.

If an entry sets both `group_by` and `dedupe_key`, validation returns a warning and grouping wins. `dedupe_key` and `cooldown` are ignored for that entry.

### Grouping

Set `group_by` to collapse many related events into one ticket, for example one ticket per OTA deployment instead of one per failed device.

1. When an event passes `filter`, the platform evaluates `group_by` and joins the results into a group key.
2. Events with the same key (for the same workflow version and event) collect in a buffer.
3. When the `group_wait` window closes, the platform opens one ticket for the group.
4. `title`, `tags` and `metadata` are evaluated once, against the **latest** event in the buffer, with `group.event_count`, `group.first_fired_at` and `group.key` filled in. `filter` is checked again against that latest event.

| Setting | Behavior |
| - | - |
| `group_wait_mode: fixed` | The window starts at the first event and does not move. The ticket opens `group_wait` after the first event. |
| `group_wait_mode: debounce` | Each new event restarts the window. The ticket opens once no event has arrived for `group_wait`. |
| `group_wait: 0s` | No buffer. Each event opens a ticket right away, with `group.event_count` of `1`. Combine with `group_interval` to rate-limit per group key. |
| `group_interval` | After a group opens a ticket, further events for that key are **dropped** (not buffered) until the interval ends. The next event after that starts a new group. |

> [!NOTE]
>
> If opening the group ticket fails, that batch of events is discarded. The next matching event starts a new group.

### Example: device updates with dedup

One ticket per device per 30 minutes, however often the device changes. This is the `maintenance-request` workflow used across these pages.

```yaml theme={null}
name: maintenance-request
kind: work_order
initial: new
labels:
  extra:
    - { key: new,         class: open,             color: "#9CA3AF" }
    - { key: in_progress, class: in_progress,      color: "#8B5CF6" }
    - { key: resolved,    class: terminal_success, color: "#10B981", terminal: true }
    - { key: cancelled,   class: cancelled,        color: "#6B7280", terminal: true }
trigger_intake:
  - event: device.device.updated.v1
    filter: 'event.payload.project_id == project.id'
    title: '"Check device: " + event.payload.device_id'
    dedupe_key: '"maint:" + event.payload.device_id'
    cooldown: "30m"
    tags:
      - kind: device
        value: "event.payload.device_id"
    metadata:
      source_event: "event.key"
transitions:
  - { from: new,         to: in_progress, permit: [project.member] }
  - { from: new,         to: cancelled,   permit: [project.member] }
  - { from: in_progress, to: resolved,    permit: [project.member] }
  - { from: in_progress, to: cancelled,   permit: [project.member] }
```

### Example: grouped OTA failures

One ticket per failing deployment. The ticket opens after five quiet minutes, and the same deployment cannot open another ticket for an hour.

```yaml theme={null}
trigger_intake:
  - event: ota.deployment.failed.v1
    title: '"OTA failures in deployment " + event.payload.deployment_id + " (" + string(group.event_count) + " devices)"'
    group_by:
      - 'event.payload.deployment_id'
    group_wait: "5m"
    group_wait_mode: debounce
    group_interval: "1h"
    metadata:
      deployment_id: "event.payload.deployment_id"
      last_failure_reason: "event.payload.reason"
      first_failure_at: "group.first_fired_at"
    tags:
      - kind: device
        value: "event.payload.device_id"
```

The `device` tag and `last_failure_reason` come from the latest failure in the group. Add this block to a workflow like `maintenance-request` above, then validate and activate it.

***

## Alarm-triggered tickets

An [alarm](/alarms/overview.md) can open a work-order ticket each time one of its instances activates. You configure this on the **alarm definition**, not with `trigger_intake`.

In the Console, open the alarm definition and turn on **Work order on activation**. Then choose the **Workflow** and, optionally, a **Dedup key**. Over the API, set these fields on the alarm definition:

| Field | Description |
| - | - |
| `auto_create_wo` | `true` opens a ticket when an instance activates. |
| `wo_workflow_def_id` | Workflow to open the ticket in. If empty, the platform uses the project's active workflow with `kind: work_order`. |
| `wo_dedup_key_cel` | Optional CEL expression for the dedup key. Defaults to one bucket per asset. |
| `wo_cooldown` | Optional dedup window, such as `24h` or `12 hours`. Defaults to `24h`. `0s` turns dedup off. API only; the Console has no field for it. |

When an instance activates, the platform:

1. Resolves the active version of the chosen workflow, or the project's `work_order` workflow. If there is none, no ticket opens.
2. Checks the dedup bucket, using the same rules as [Deduplication](#deduplication).
3. Opens a ticket titled `Alarm <alarm definition ID>` in the initial label, tagged `asset` (the alarm's asset) and `alarm_instance` (the instance ID).

The alarm instance must have an asset. Activations without one do not open a ticket.

`wo_dedup_key_cel` is evaluated against `event.payload` (the activation) and `project.id`. Useful payload fields are `asset_id`, `alarm_def_id`, `alarm_instance_id`, `priority` and `trigger_value`. For example, one work order per asset and alarm definition per window:

```
event.payload.asset_id + ":" + event.payload.alarm_def_id
```

> [!WARNING]
>
> Only `event`, `project` and `group` exist in this expression. Expressions that reference other variables, such as `labels` or `alarm`, fail and no ticket opens.

> [!NOTE]
>
> Dedup suppresses repeat tickets within the window. It does not attach the new activation to the existing ticket.

***

## Due dates and due policy

A ticket's deadline is its top-level `due_at` field (RFC3339). Set it in the create request or with `PATCH …/tickets/{ticket_id}`. A workflow's `due_policy` then decides what happens as the deadline approaches and passes.

> [!WARNING]
>
> The Console's **Due date** field writes `metadata.due_at`, not the top-level `due_at`. It does not start due-policy timers. To get timers, set `due_at` through the API.

### `due_policy` fields

```yaml theme={null}
due_policy:
  lead_time: 4h
  on_breach:
    to: overdue
```

| Field | Behavior |
| - | - |
| `lead_time` | Optional. Positive duration. Publishes **Ticket Due Soon** at `due_at` minus `lead_time`. Skipped if that time has already passed. |
| `on_breach.to` | Optional. Label to move the ticket to when `due_at` passes. |

Validation rejects `on_breach.to` unless every declared transition into that label includes `system` in its `permit` list, and at least one such transition exists.

### What happens at each time

| When | What the platform does |
| - | - |
| `due_at` − `lead_time` | Publishes `ticket.ticket.due_approaching.v1` (**Ticket Due Soon**). |
| `due_at` | Publishes `ticket.ticket.due_reached.v1` (**Ticket Due**). Then, if `on_breach.to` is set, applies the breach transition. |

The breach transition runs only if the workflow declares a transition from the ticket's **current** label to `on_breach.to` with `system` in `permit`. It runs as the system actor with reason `due policy breach`, and it still has to pass:

* The transition's guards.
* Any `before_transition` rule. A transition that requires a form is skipped, because a timer cannot fill one in.
* The sub-ticket check. A ticket with open sub-tickets cannot move to a terminal label.

If any check fails, the ticket stays where it is. **Ticket Due** is still published.

Timer rules:

* Each event is published once per deadline, even if delivery is retried.
* Patching `due_at` moves both timers. A timer for a deadline that has since moved later is ignored.
* If `due_at` is in the past or less than 10 seconds away when you set it, **Ticket Due** fires about 10 seconds later.
* Moving a ticket to a terminal label cancels its timers. Timers that still fire for a ticket in `terminal_success`, `terminal_failed` or `cancelled` are ignored.
* Timers follow the `due_policy` of the workflow version the ticket is on, not the newest version.

### Notifications

**Ticket Due Soon** and **Ticket Due** are notification event types with the recipient roles **Assignee** and **Creator**. Subscribe to them like any other ticket event. See [Assignment and notifications](/ticketing/assignment-notifications.md).

### Example: maintenance SLA with escalation

```yaml theme={null}
name: maintenance-request
kind: work_order
initial: new
labels:
  extra:
    - { key: new,         class: open,             color: "#9CA3AF" }
    - { key: in_progress, class: in_progress,      color: "#8B5CF6" }
    - { key: overdue,     class: in_progress,      color: "#EF4444" }
    - { key: resolved,    class: terminal_success, color: "#10B981", terminal: true }
    - { key: cancelled,   class: cancelled,        color: "#6B7280", terminal: true }
due_policy:
  lead_time: 4h
  on_breach:
    to: overdue
transitions:
  - { from: new,         to: in_progress, permit: [project.member] }
  - { from: new,         to: cancelled,   permit: [project.member] }
  - { from: in_progress, to: resolved,    permit: [project.member] }
  - { from: in_progress, to: cancelled,   permit: [project.member] }
  - { from: new,         to: overdue,     permit: [system] }
  - { from: in_progress, to: overdue,     permit: [system] }
  - { from: overdue,     to: resolved,    permit: [project.member] }
  - { from: overdue,     to: cancelled,   permit: [project.member] }
```

Create a ticket with a deadline:

```bash theme={null}
curl -sS -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "workflow_def_id": "'"$WORKFLOW_DEF_ID"'",
    "title": "Replace conveyor belt on line 2",
    "due_at": "2026-10-10T18:00:00Z"
  }' \
  "$API/projects/$PROJECT_ID/tickets"
```

***

## Transition actions

Attach actions to a transition to call other systems when a ticket changes state:

* `on_exit` actions run when a ticket leaves the `from` label through this transition.
* `on_enter` actions run when it arrives in the `to` label through this transition.

Both lists live on the transition, so they run only for that specific `from` → `to` move.

### How actions run

1. The transition commits. The actions are queued as part of the same change, so a transition never commits without its actions, or the other way round.
2. The platform picks up queued actions within a few seconds and runs them in the background. The transition request does not wait for them.
3. For one transition, `on_exit` actions start before `on_enter` actions.
4. A failed action is retried after 15 seconds, up to 3 attempts in total. After the third failure it is abandoned. The ticket stays in its new label either way.
5. Each action is retried on its own. One failing action does not stop the others.

**Version pinning.** Actions come from the workflow version the ticket is on, and are captured when the transition commits. Activating a new version does not change actions for tickets already on the old version. Only tickets created after activation use the new version.

### `ticket.http`

Calls an HTTP endpoint. A response with status `200`–`299` is a success; anything else counts as a failure and is retried.

| Field | Required | Description |
| - | - | - |
| `kind` | Yes | `ticket.http` |
| `url` | Yes | `http` or `https` URL. The host must resolve in DNS when you validate the workflow. Hosts that resolve to private or link-local IP addresses are rejected at validation and again before each call. |
| `method` | No | `GET` (default), `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` or `OPTIONS` |
| `headers` | No | Map of header name to value |
| `body` | No | JSON value sent as the request body |
| `bindings` | No | Values to copy from the response into ticket metadata |

`url`, `headers` and `body` are sent exactly as written. They are not templates and are not evaluated per ticket, so you cannot insert the ticket ID or metadata into them. When you need per-ticket values, use `ticket.rpc`, whose fields are CEL.

**Bindings** copy parts of a successful response into the ticket's `metadata`:

| Field | Description |
| - | - |
| `from` | `status_code`, `headers`, `body` or `content_type`. Use dots to reach into a JSON body, for example `body.result.score`. |
| `writes_to.kind` | `metadata`. It is the only kind supported for action bindings. |
| `writes_to.metadata_path` | Metadata key to write. Dots create nested objects, for example `enrichment.score`. |

If a `from` path is missing from the response, the action fails and is retried.

### `ticket.rpc`

Queues a call to a typed method on an edge device. It uses [Device RPC](/edge/device-rpc/overview.md) queued invoke, so the call waits for the device if it is offline. See [Queued invoke](/edge/device-rpc/queued-invoke.md).

| Field | Required | Description |
| - | - | - |
| `kind` | Yes | `ticket.rpc` |
| `device_id` | Yes | CEL expression returning the device UUID as a string |
| `service_name` | Yes | Fully qualified gRPC service name on the device |
| `method_name` | Yes | Method name |
| `fields` | No | Map of request field name to CEL expression. The results form the JSON request body. |
| `ttl_seconds` | No | How long the queued call stays valid, from `60` to `86400` |

CEL expressions are evaluated when the action runs, against the ticket as it is at that moment:

| Variable | Contents |
| - | - |
| `ticket` | `id`, `metadata`, `assigned_to`, `assigned_group_id`, `tagged_user_ids`, and `tags` (tag kind to list of values) |
| `actor` | `id` and `kind` of whoever made the transition |
| `event.payload` | The `form_payload` sent with the transition, if any |
| `project.id` | The project UUID |
| `now` | Current time |

The action succeeds once the call is queued. Delivery to the device and the method's result are tracked by Device RPC, not by the ticket. Make the device method safe to run twice, because a retried action can queue the call again.

### Example: notify and enrich

When a request moves to `in_progress`, look up the device in an asset system and post to a chat webhook. When it is resolved, tell the device.

```yaml theme={null}
transitions:
  - from: new
    to: in_progress
    permit: [project.member]
    on_enter:
      - kind: ticket.http
        url: https://assets.example.com/api/lookup
        method: GET
        headers:
          X-Api-Key: replace-with-your-key
        bindings:
          - from: body
            writes_to: { kind: metadata, metadata_path: asset_lookup }
          - from: body.location
            writes_to: { kind: metadata, metadata_path: location }
      - kind: ticket.http
        url: https://hooks.example.com/maintenance
        method: POST
        body:
          text: A maintenance request is in progress
  - from: in_progress
    to: resolved
    permit: [project.member]
    on_enter:
      - kind: ticket.rpc
        device_id: 'ticket.tags.device[0]'
        service_name: acme.maintenance.v1.MaintenanceService
        method_name: AcknowledgeWorkOrder
        fields:
          ticket_id: 'ticket.id'
          resolved_by: 'actor.id'
        ttl_seconds: 3600
```

Replace the `example.com` hosts with your own. Validation resolves each host, so placeholder hosts fail.

`ticket.tags.device[0]` reads the first `device` tag, which the dedup example above adds at intake. If a ticket has no `device` tag, the expression fails and the action is abandoned after three attempts.

> [!WARNING]
>
> Header values are stored in the workflow spec as plain text and are visible to anyone who can read the workflow. Prefer endpoints that authenticate with a scoped, revocable key.

***

## Workflow templates

Workflow templates are platform-provided workflows that you install into a project as your own editable copy. The catalog currently includes `cmms.work_order`, a work-order workflow with intake and completion forms.

### List templates

```
GET /core/api/v1/projects/{project_id}/ticket-workflow-templates
```

**Permission:** project read access

```bash theme={null}
curl -sS \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  "$API/projects/$PROJECT_ID/ticket-workflow-templates"
```

**Response `data.items[]` fields:**

| Field | Type | Description |
| - | - | - |
| `key` | string | Template key, for example `cmms.work_order` |
| `name` | string | Name of the workflow it installs, for example `work_order_v1` |
| `kind` | string | Workflow kind, for example `work_order` |
| `revision` | number | Template revision |
| `description` | string | Description, when set |
| `is_active` | boolean | Whether the template is offered |
| `created_at`, `updated_at` | string | RFC3339 timestamps |
| `forms` | array | Bundled forms: `key`, `name`, `version`, `revision`, `ordinal` |

The catalog is the same for every project.

### Install a template

```
POST /core/api/v1/projects/{project_id}/ticket-workflow-templates/{template_key}/instantiate
```

**Permission:** project **manage assets**

```bash theme={null}
curl -sS -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  "$API/projects/$PROJECT_ID/ticket-workflow-templates/cmms.work_order/instantiate"
```

```json theme={null}
{
  "ok": 1,
  "data": {
    "item": {
      "workflow_def_id": "8f0c…",
      "workflow_version_id": "1b7e…",
      "version": 1,
      "form_ids": ["3d2a…", "c41f…"],
      "already_installed": false
    }
  }
}
```

| Outcome | Result |
| - | - |
| First install | Creates the forms and the workflow, and **activates** it. `already_installed` is `false`. |
| Already installed | Recreates any bundled form you deleted and leaves your workflow untouched. Returns your current active version, with `already_installed` set to `true`. |
| Name taken | `409` if a form or workflow you created yourself already uses the template's name. |
| Unknown key | `404` |

The `cmms.work_order` workflow is named `work_order_v1` with `kind: work_order`, so alarms with **Work order on activation** and no explicit workflow use it. It includes `due_policy.lead_time: 24h` and no `on_breach`. To add triggers, escalation or actions, edit your copy like any workflow, then validate and activate the new version.

***

## Next steps

* [Workflows](/ticketing/workflows.md) — author, validate and activate specs
* [Forms](/ticketing/forms.md) — collect structured data on create and transition
* [Assignment and notifications](/ticketing/assignment-notifications.md) — route due and lifecycle events to people
* [API reference](/ticketing/api-reference.md) — ticket, workflow and template routes
* [Troubleshooting](/ticketing/troubleshooting.md) — when automation does not fire


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.