# Product Groups

Product groups let you manage a set of related products as one catalog unit. You can use them to control contract windows, activation state, publishing windows, and booking expiry settings across multiple products.

In practice, a product group helps you organise products that should follow the same commercial rules. This is useful when you want consistent configuration for a cohort of products instead of updating each product one by one.

## Key Concepts

### Contract windows

Each product group has contract date boundaries:

* `contractEarliestStartDate`
* `contractLatestStartDate`
* `contractEarliestEndDate`
* `contractLatestEndDate`

`contractEarliestStartDate` and `contractLatestEndDate` are the outer bounds that control the overall date range a booking can be made over for products in this group. No booking can start before `contractEarliestStartDate` or end after `contractLatestEndDate`.

`contractLatestStartDate` and `contractEarliestEndDate` define the inner flexibility within those outer bounds:

* A booking can start anywhere between `contractEarliestStartDate` and `contractLatestStartDate` (e.g. a student arriving late).
* A booking can end anywhere between `contractEarliestEndDate` and `contractLatestEndDate` (e.g. a student leaving early).

In other words, the full window a booking may occupy is `[contractEarliestStartDate, contractLatestEndDate]`, while the start date must fall in `[contractEarliestStartDate, contractLatestStartDate]` and the end date must fall in `[contractEarliestEndDate, contractLatestEndDate]`.

### State and publishing

A product group has two related controls:

* **Activation state** (`ACTIVE`, `INACTIVE`)
* **Publishing window** (`startDate`, `endDate`) with a publishing state

You can update activation state for one or more product groups in a single mutation.

### Product segments

Product groups can be classified by segment to indicate the type of student accommodation they represent. The `productSegment` field is optional and applies to student product groups only.

| Value           | Description                                                     |
| --------------- | --------------------------------------------------------------- |
| `FULL_YEAR`     | Accommodation spanning the full calendar year including summer  |
| `ACADEMIC_YEAR` | Accommodation covering the full academic year, excluding summer |
| `SEMESTER_1`    | Accommodation for the first semester of the academic year only  |
| `SEMESTER_2`    | Accommodation for the second semester of the academic year only |

Omit `productSegment` for non-student product groups.

### Product membership

A product group contains a connection of products. This lets you inspect the products that belong to the group and paginate through them.

## GraphQL API

### Type

#### `ProductGroup`

| Field                            | Type                            | Description                                    |
| -------------------------------- | ------------------------------- | ---------------------------------------------- |
| `id`                             | `ID!`                           | Product group identifier                       |
| `internalReference`              | `String!`                       | Internal reference string for the group        |
| `workspaceId`                    | `Integer!`                      | Workspace identifier                           |
| `contractEarliestStartDate`      | `String!`                       | Earliest contract start date                   |
| `contractLatestStartDate`        | `String!`                       | Latest contract start date                     |
| `contractEarliestEndDate`        | `String!`                       | Earliest contract end date                     |
| `contractLatestEndDate`          | `String!`                       | Latest contract end date                       |
| `defaultExpiryDurationInSeconds` | `Integer`                       | Default booking expiry duration                |
| `allowBookingExpiryTimer`        | `Boolean`                       | Whether booking expiry timer is enabled        |
| `academicYear`                   | `String`                        | Academic year in `YYYY/YYYY` format            |
| `productSegment`                 | `CatalogProductSegmentEnum`     | Segment classification (student products only) |
| `state`                          | `CatalogProductGroupStateEnum!` | Activation state                               |
| `publishing`                     | `Publishing!`                   | Publishing window and lifecycle state          |
| `contractTemplateExternalId`     | `String`                        | External contract template identifier          |
| `createdAt`                      | `ISO8601DateTime!`              | Creation timestamp                             |
| `updatedAt`                      | `ISO8601DateTime!`              | Last update timestamp                          |

#### `CatalogProductGroupStateEnum`

| Value      | Description               |
| ---------- | ------------------------- |
| `ACTIVE`   | Product group is active   |
| `INACTIVE` | Product group is inactive |

#### `CatalogProductSegmentEnum`

| Value           | Description                                                     |
| --------------- | --------------------------------------------------------------- |
| `FULL_YEAR`     | Accommodation spanning the full calendar year including summer  |
| `ACADEMIC_YEAR` | Accommodation covering the full academic year, excluding summer |
| `SEMESTER_1`    | Accommodation for the first semester of the academic year only  |
| `SEMESTER_2`    | Accommodation for the second semester of the academic year only |

#### `CatalogPublishStateEnum`

| Value       | Description                    |
| ----------- | ------------------------------ |
| `SCHEDULED` | Publishing has not started yet |
| `ONGOING`   | Publishing is currently active |
| `FINISHED`  | Publishing window has ended    |

### Queries

#### Get a product group

```graphql
query ProductGroup($id: ID!) {
  catalog {
    productGroup(id: $id) {
      id
      internalReference
      state
      productSegment
      publishing {
        startDate
        endDate
        state
      }
      products {
        totalCount
      }
    }
  }
}
```

```json
{
  "id": "product-group-id"
}
```

#### List product groups

```graphql
query ProductGroups(
  $filters: ProductGroupsFiltersInput
  $first: Int
  $after: String
) {
  catalog {
    productGroups(filters: $filters, first: $first, after: $after) {
      nodes {
        id
        internalReference
        state
      }
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}
```

```json
{
  "filters": {
    "state": "ACTIVE",
    "published": true
  },
  "first": 20
}
```

> **Tip:** `productGroups` follows the standard connection pattern. Use cursors for pagination when iterating through large catalogs.

### Mutations

#### Create a product group

```graphql
mutation CreateProductGroup($input: CreateProductGroupMutationInput!) {
  catalog {
    productGroup {
      create(input: $input) {
        id
      }
    }
  }
}
```

```json
{
  "input": {
    "productGroup": {
      "internalReference": "PG-2026-01",
      "contractEarliestStartDate": "2026-01-01",
      "contractLatestStartDate": "2026-09-30",
      "contractEarliestEndDate": "2026-01-15",
      "contractLatestEndDate": "2027-08-31",
      "workspaceId": "workspace-id",
      "academicYear": "2026/2027",
      "productSegment": "ACADEMIC_YEAR",
      "allowBookingExpiryTimer": true,
      "defaultExpiryDurationInSeconds": 1800,
      "publishing": {
        "startDate": "2026-02-01",
        "endDate": "2027-06-30"
      }
    }
  }
}
```

Required fields for `productGroup` in create:

| Field                       | Type           |
| --------------------------- | -------------- |
| `internalReference`         | `String!`      |
| `contractEarliestStartDate` | `ISO8601Date!` |
| `contractLatestStartDate`   | `ISO8601Date!` |
| `contractEarliestEndDate`   | `ISO8601Date!` |
| `contractLatestEndDate`     | `ISO8601Date!` |
| `workspaceId`               | `ID!`          |
| `academicYear`              | `String!`      |

#### Update a product group

```graphql
mutation UpdateProductGroup($input: UpdateProductGroupMutationInput!) {
  catalog {
    productGroup {
      update(input: $input) {
        id
      }
    }
  }
}
```

```json
{
  "input": {
    "productGroupId": "product-group-id",
    "productGroup": {
      "internalReference": "PG-2026-01-REV1",
      "productSegment": "SEMESTER_1",
      "publishing": {
        "startDate": "2026-03-01",
        "endDate": "2027-07-31"
      }
    }
  }
}
```

Required fields for update input:

| Field            | Type                       |
| ---------------- | -------------------------- |
| `productGroupId` | `ID!`                      |
| `productGroup`   | `ProductGroupUpdateInput!` |

#### Update product group state in bulk

```graphql
mutation UpdateProductGroupState($input: UpdateProductGroupsStateMutationInput!) {
  catalog {
    productGroup {
      updateState(input: $input) {
        updatedCount
        productGroupIds
      }
    }
  }
}
```

```json
{
  "input": {
    "productGroupIds": [
      "product-group-id-1",
      "product-group-id-2"
    ],
    "state": "ACTIVE"
  }
}
```

## MCP Tools

If you use MCP, these tools are relevant:

* [get\_product\_group](/mcp/tools/catalog/product-groups/get_product_group.md)
* [update\_product\_groups](/mcp/tools/catalog/product-groups/update_product_groups.md)
* [get\_product](/mcp/tools/catalog/products/get_product.md)

See [Tools](/mcp/tools.md) for setup and full coverage.

## Relationships

* A product group belongs to a [Workspaces](/workspaces/workspaces.md)
* A product group has many [Products](/catalog/products.md)
* Product groups work alongside [Pricing](/pricing/pricing.md) to control what can be sold and when


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.lavanda.app/catalog/product-groups.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
