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

# Assignment and notifications

> Assign tickets to a user or group, let group members claim work, and notify assignees, creators, or fixed recipients when tickets change.

Every ticket has at most one **assignee**: a single user or a single user group. Assignment decides who can take transitions permitted to `ticket.assignee` or `ticket.assignee_group_member`, and who receives notifications addressed to the `assignee` role.

Set these before running the examples:

```bash theme={null}
export API=https://api.ilyama.golain.io/core/api/v1
export TOKEN=…          # OIDC access token (same as Console)
export ORG_ID=…         # Organization UUID
export PROJECT_ID=…
export TICKET_ID=…
```

Every request sends `Authorization: Bearer $TOKEN` and `ORG-ID: $ORG_ID`. Successful responses use the envelope `{"ok": 1, "data": {...}}`.

## Assignment

### User vs group

| Field on request | Field on ticket | Value | Check |
| - | - | - | - |
| `assigned_user_id` | `assigned_to` | User ID | The user must be a member of the ticket's **Project** (`400` "user is not a project member") |
| `assigned_group_id` | `assigned_group_id` | User group UUID | The group must belong to your **Organization** (`400` "user group not found in this organization") |

The two are mutually exclusive:

* Send exactly one of them. Sending both (or neither, on `/assign`) returns `400` "exactly one of assigned\_user\_id or assigned\_group\_id is required".
* Assigning a user clears `assigned_group_id`. Assigning a group clears `assigned_to`.

### Assign when you create a ticket

Add `assigned_user_id` or `assigned_group_id` to the create body. The ticket is created and assigned in one step.

```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 filter on compressor 3",
    "assigned_group_id": "'"$GROUP_ID"'"
  }' \
  "$API/projects/$PROJECT_ID/tickets"
```

Creating with an assignee raises both **Ticket Created** and **Ticket Assigned** notifications.

### Assign and unassign an existing ticket

| Method | Path | Body | Permission |
| - | - | - | - |
| `POST` | `/projects/{project_id}/tickets/{ticket_id}/assign` | Exactly one of `assigned_user_id` / `assigned_group_id` | Project member |
| `POST` | `/projects/{project_id}/tickets/{ticket_id}/unassign` | `{"target": "user"}` or `{"target": "group"}` | Project member |

Both return `{"ok": 1, "data": {"ok": true}}`.

```bash Assign a user theme={null}
curl -sS -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{"assigned_user_id": "'"$USER_ID"'"}' \
  "$API/projects/$PROJECT_ID/tickets/$TICKET_ID/assign"
```

```bash Assign a group theme={null}
curl -sS -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{"assigned_group_id": "'"$GROUP_ID"'"}' \
  "$API/projects/$PROJECT_ID/tickets/$TICKET_ID/assign"
```

```bash Unassign the user theme={null}
curl -sS -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{"target": "user"}' \
  "$API/projects/$PROJECT_ID/tickets/$TICKET_ID/unassign"
```

Repeating an assignment that is already in place, or unassigning a target that is already empty, succeeds without changing anything and sends no notification.

> [!NOTE]
>
> The Console has no assign or unassign control. Its **Assign users** option on the create form adds `user` tags to the ticket; it does not set `assigned_to`. See [Console](/ticketing/console).

### Group members claiming a ticket

There is no separate claim endpoint. A member claims a group ticket by assigning it to themselves:

1. Assign the ticket to a group (for example, a maintenance shift).
2. A member of that group calls `/assign` with their own user ID as `assigned_user_id`.
3. The ticket moves from the group to that user: `assigned_to` is set and `assigned_group_id` is cleared.

Any Project member can call `/assign`, so the platform does not check that the claimer belongs to the group.

In Slack, Microsoft Teams, and Telegram, notification cards for **Ticket Created**, **Ticket Transitioned**, and **Ticket Unassigned** include an **Assign to me** button. It assigns the ticket to the Golain user linked to the chat identity who clicked, and requires that user to be a member of the Organization and Project.

### How assignment affects permits

Transitions declare who may fire them with `permit`. These permits read assignment:

| Permit | Who matches |
| - | - |
| `ticket.assignee` | The user in `assigned_to`, or any user tagged on the ticket with a `user` tag |
| `ticket.tagged_user` | Same as `ticket.assignee` |
| `ticket.assignee_group_member` | Any member of the group in `assigned_group_id`. Matches nobody when no group is assigned. |
| `ticket.creator` | The user in `created_by` |

> [!WARNING]
>
> Claiming a group ticket clears `assigned_group_id`. After a claim, `ticket.assignee_group_member` matches nobody and only the claimer matches `ticket.assignee`. If other group members still need to move the ticket, also permit `ticket.assignee` or a project permission such as `project.member` on those transitions.

A workflow where any member of the assigned group can start and finish work:

```yaml theme={null}
name: maintenance-request
initial: new
labels:
  extra:
    - key: new
      class: open
    - key: in_progress
      class: in_progress
    - key: resolved
      class: terminal_success
      terminal: true
    - key: cancelled
      class: cancelled
      terminal: true
transitions:
  - from: new
    to: in_progress
    permit: [ticket.assignee_group_member, ticket.assignee]
  - from: in_progress
    to: resolved
    permit: [ticket.assignee_group_member, ticket.assignee]
  - from: new
    to: cancelled
    permit: [project.member]
  - from: in_progress
    to: cancelled
    permit: [project.member]
```

See [Workflows](/ticketing/workflows) for the full permit list.

### List tickets by assignee

Filter `GET /projects/{project_id}/tickets` with:

| Query | Matches |
| - | - |
| `assigned_to` | Tickets assigned to this user ID |
| `assigned_group_id` | Tickets assigned to this group UUID |

Combine with other filters such as `system_class` or `labels`.

```bash theme={null}
curl -sS \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  "$API/projects/$PROJECT_ID/tickets?assigned_group_id=$GROUP_ID&system_class=open&fields=id,title,assigned_to,assigned_group_id,state_label"
```

Tickets a group member has claimed no longer match `assigned_group_id`. To find them, filter by `assigned_to`.

## Notifications for tickets

Ticket notifications use **notification subscriptions**. A subscription selects events, a channel (email, Slack, Microsoft Teams, Telegram, Twilio SMS, or Twilio WhatsApp), and who receives each message.

### Ticket event types

| Event type | Name | Recipient roles |
| - | - | - |
| `ticket.ticket.created.v1` | **Ticket Created** | `assignee`, `creator`, `actor` |
| `ticket.ticket.updated.v1` | **Ticket Updated** (title, body, or metadata changed) | `assignee`, `creator`, `actor` |
| `ticket.ticket.transitioned.v1` | **Ticket Transitioned** | `assignee`, `creator`, `actor` |
| `ticket.ticket.reached_terminal.v1` | **Ticket Closed** | `assignee`, `creator` |
| `ticket.ticket.assigned.v1` | **Ticket Assigned** | `assignee`, `creator`, `actor` |
| `ticket.ticket.unassigned.v1` | **Ticket Unassigned** | `creator`, `actor` |
| `ticket.ticket.due_approaching.v1` | **Ticket Due Soon** | `assignee`, `creator` |
| `ticket.ticket.due_reached.v1` | **Ticket Due** | `assignee`, `creator` |

Ticket deletion is not a notifiable event. **Ticket Due Soon** and **Ticket Due** fire only for tickets with a top-level `due_at` on a workflow that defines `due_policy`. See [Automation](/ticketing/automation).

To list every notifiable event with its roles, call the registry:

```bash theme={null}
curl -sS \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  "$API/notification-event-registry"
```

`data.events[]` items include `key`, `display_name`, `description`, `card_fields`, `default_actions`, and `recipient_roles`.

### What a notification includes

| Content | Source |
| - | - |
| Headline | `#<number>: <title>` |
| **Project** | Project name |
| **Workflow** | Workflow name |
| **State** | Current state label (**Final state** on **Ticket Closed**; **From** / **To** on **Ticket Transitioned**) |
| **Assignee**, **By**, **Created by** | Depending on the event |
| **Due at**, **Lead time** | Due events only |
| **Tags** | Tags on the ticket |
| **View Ticket** | Opens the workflow's ticket board in the Console |

### Where subscriptions live

| Scope | Path | Write permission |
| - | - | - |
| Organization | `/organizations/notification-subscriptions` | Organization **manage integrations** |
| Project | `/projects/{project_id}/notification-subscriptions` | Project **manage integrations** |
| Fleet | `/projects/{project_id}/fleets/{fleet_id}/notification-subscriptions` | Project **manage integrations** |

Each scope supports `GET` (list), `POST` (create), `PATCH /{id}`, `DELETE /{id}`, and `POST /{id}/test`. The delivery log and single-subscription `GET` are on the organization path: `GET /organizations/notification-subscriptions/{id}` and `/{id}/deliveries`. They work for subscriptions of any scope.

### Subscription fields

| Field | Required | Description |
| - | - | - |
| `subscribed_events` | Yes | Event types from the table above |
| `channel_ref` | Yes | Destination. For email: `{"kind": "email", "to": ["…"]}`. `to` is optional when you set `recipient_roles`. |
| `account_id` | For non-email channels | Integration account UUID (Slack, Microsoft Teams, Telegram, Twilio) |
| `workflow_def_ids` | No | Only send for tickets on these workflow definitions. Empty means every workflow. Every ID must be a ticket workflow in the subscription's scope, or the write fails with `400`. |
| `recipient_roles` | No | `assignee`, `creator`, and/or `actor`. Email channels only. |
| `notify_actor` | No | Default `false`: the person who caused the event is not notified through another role |
| `group_assignee_mode` | No | `members` (default) or `none`. Applies when `assignee` resolves to a group. |
| `enabled` | No | Default `true` |
| `template_id` | No | Pin a notification template. Empty uses the most specific active template, else the built-in card. |

On `PATCH`, omitted fields stay unchanged. `workflow_def_ids: []` clears the workflow filter, and `recipient_roles: []` returns to a fixed-destination subscription.

### Recipient roles

| Role | Resolves to |
| - | - |
| `assignee` | The assigned user, or each member of the assigned group (with `group_assignee_mode: members`). On **Ticket Assigned**, the new assignee. |
| `creator` | The user who opened the ticket |
| `actor` | The user who caused the event. Asking for `actor` always includes them, whatever `notify_actor` says. |

Rules enforced when you save:

* Every role you list must be declared by every subscribed event. For example, `assignee` with **Ticket Unassigned** returns `400` "role "assignee" is not declared by ticket.ticket.unassigned.v1".
* `recipient_roles` works only with `channel_ref.kind: "email"`.
* An email subscription with no `to` and no `recipient_roles` is rejected.
* `$`-placeholders in `to` (such as `$assignee_email`) are no longer supported and are rejected.

Each resolved person gets their own message. A person already listed in `to` is not mailed twice.

> [!WARNING]
>
> The `assignee` role reads only `assigned_to` and `assigned_group_id`. Users added through `user` tags (including the Console's **Assign users** option) are not notified as assignees. Use the `/assign` route if those people must receive assignee notifications.

### Create a subscription for ticket events

**1. Pick the workflow**

Find the `workflow_def_id` of the workflow you want, for example `maintenance-request`, from `GET /projects/{project_id}/ticket-workflows`.

**2. Create the subscription**

This project subscription emails the assignee when a ticket is assigned or changes state, but not the person who made the change:

```bash theme={null}
curl -sS -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "subscribed_events": ["ticket.ticket.assigned.v1", "ticket.ticket.transitioned.v1"],
    "channel_ref": {"kind": "email"},
    "recipient_roles": ["assignee"],
    "notify_actor": false,
    "group_assignee_mode": "members",
    "workflow_def_ids": ["'"$WORKFLOW_DEF_ID"'"]
  }' \
  "$API/projects/$PROJECT_ID/notification-subscriptions"
```

The API returns `201` with the subscription in `data`:

```json theme={null}
{
  "ok": 1,
  "data": {
    "id": "8f0c…",
    "org_id": "…",
    "account_id": "",
    "scope_type": "project",
    "scope_id": "…",
    "subscribed_events": ["ticket.ticket.assigned.v1", "ticket.ticket.transitioned.v1"],
    "channel_ref": {"kind": "email"},
    "enabled": true,
    "health": "ok",
    "alarm_def_ids": [],
    "workflow_def_ids": ["…"],
    "recipient_roles": ["assignee"],
    "notify_actor": false,
    "group_assignee_mode": "members"
  }
}
```

**3. Add a fixed list for closures (optional)**

A second subscription can email a fixed address whenever a ticket closes:

```bash theme={null}
curl -sS -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "subscribed_events": ["ticket.ticket.reached_terminal.v1"],
    "channel_ref": {"kind": "email", "to": ["maintenance-leads@example.com"]},
    "workflow_def_ids": ["'"$WORKFLOW_DEF_ID"'"]
  }' \
  "$API/projects/$PROJECT_ID/notification-subscriptions"
```

In the Console, you create fixed-destination subscriptions from each channel's integration page with **Add subscription**. Recipient roles, `notify_actor`, `group_assignee_mode`, and `workflow_def_ids` are API-only today.

### Group assignments and claims

Pair **Ticket Assigned** with **Ticket Transitioned** on an `assignee` subscription:

1. While the ticket is assigned to a group, each transition notifies every group member except the one who moved it (with `notify_actor: false`).
2. When a member claims the ticket, **Ticket Assigned** names them as the assignee. They are also the actor, so they are not notified of their own claim.
3. From then on, `assignee` resolves to that person only. The rest of the group stops receiving updates for the ticket.

With `group_assignee_mode: none`, a group assignment notifies nobody and writes one `suppressed` row to the delivery log.

### Delivery log and suppression

```bash theme={null}
curl -sS \
  -H "Authorization: Bearer $TOKEN" \
  -H "ORG-ID: $ORG_ID" \
  "$API/organizations/notification-subscriptions/$SUBSCRIPTION_ID/deliveries?limit=50"
```

| Query | Description |
| - | - |
| `event_id` | Only rows for one event |
| `page`, `limit` | Paging. `limit` defaults to 50, maximum 500. |

Each row has `event_key`, `event_id`, `status`, `attempt_count`, `error_detail`, `recipient_user_id`, `recipient_role`, `recipient_display`, `recipient_via_group_id`, `recipient_via_group_name`, and timestamps. Fixed-destination rows have `recipient_user_id: null`. Email addresses are never returned: `recipient_contact` is null and addresses in `error_detail` are redacted.

`status` is `pending`, `delivered`, `retrying`, `failed`, or `suppressed`. A `suppressed` row is a normal outcome, not a failure. `error_detail` gives the reason, for example:

| Reason | Meaning |
| - | - |
| No assignee on this ticket | The `assignee` role found nobody |
| Assigned to a group; subscription excludes group assignments | `group_assignee_mode` is `none` |
| Group has more than N members | Over the group expansion limit (default 50) |
| Assigned group has no members / no longer exists | Nobody to expand |
| Actor of this event (notify\_actor is off) | The person who made the change |
| No email on record | The user has no email address |
| Recipient cannot access this project | The user lost Project read access |
| Already addressed by channel\_ref.to | Already on the fixed list |

Only users with **manage integrations** on the subscription's Organization or Project see delivery rows. Others get an empty list. In the Console, open a subscription's **Delivery log** from the subscription list.

### Test sends

`POST …/notification-subscriptions/{id}/test` sends a test message and waits up to 10 seconds for the result. Success returns `{"ok": 1, "data": {"delivered": true}}`.

> [!NOTE]
>
> A test needs a fixed destination. Role recipients resolve from a real ticket event, so testing a role-only email subscription returns `400` "test send needs a fixed channel\_ref.to". Add an address to `channel_ref.to` to test it. Disabled or suspended subscriptions also return `400`.

## Next steps

* [Workflows](/ticketing/workflows): permits, guards, and due policy
* [Automation](/ticketing/automation): due-date timers and transition actions
* [API reference](/ticketing/api-reference): every ticket route
* [Troubleshooting](/ticketing/troubleshooting): permit and notification errors


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