# Queries

Every CMS GraphQL query and mutation, with arguments and examples


A reference for website and administrative GraphQL operations in the erxes CMS.

Work through [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup) first. Execute these documents on the server with the credential appropriate to the operation. The browser example exposes only a fixed published-post search endpoint.

## Naming convention: `cp*` versus `cms*`

The CMS exposes two parallel families of operations.

CMS `cp*` operations serve client-portal integrations. Operations marked `forClientPortal: true` bypass the normal team-member permission wrapper; inspect each resolver's context and argument handling. `cpPostList`, for example, uses the Client Portal identified by `x-app-token`. See [`wrapperResolvers`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/wrapperResolvers.ts) and [`cpPostList`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts).

`cms*` operations are for the erxes admin UI and authenticated tooling. They go through the permission wrapper, they require a logged-in team member or an Apps token, and most of them require an explicit `clientPortalId` argument: `cmsPages`, `cmsPageList`, `cmsMenus`, `cmsMenuList`, and `cmsMenu` all throw `clientPortalId is required` without one. See [`queries/page.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/page.ts#L8-L53) and [`queries/menu.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/menu.ts#L6-L60).

The examples below are an API reference, not a browser operation allow-list. `cp*` does not imply published-only results or read-only behavior. Public post lists must enforce `status: published`; detail and page results need their own publication checks before exposure. Administrative `cms*` integrations can run on an authorized server without exposing their credentials to visitors.

## Pagination: two incompatible styles

This is the single most common source of `Unknown argument` errors.

Most list operations use the shared **cursor** arguments, defined once in [`apollo/constants.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/constants.ts#L1-L16). The shared cursor helper defaults to 20 items and rejects limits outside 1–100:

- `limit: Int`
- `cursor: String`
- `cursorMode: CURSOR_MODE`: `inclusive` or `exclusive`
- `direction: CURSOR_DIRECTION`: `forward` or `backward`
- `orderBy: JSON`
- `sortMode: String`
- `aggregationPipeline: [JSON]`

These are backend schema capabilities. Do not pass arbitrary browser-supplied aggregation pipelines, ordering objects, or filters through a public website endpoint. Fix the operation and validate the inputs that the website actually supports.

`page` and `perPage` are **not** part of that set. They belong to the offset style, which in the CMS is used by exactly one operation: `cpPostListWithPagination`. Its arguments come from [`GQL_OFFSET_PARAM_DEFS`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/constants.ts#L11-L16): `page`, `perPage`, `sortField`, `sortDirection`.

List operations that paginate return an envelope, not a bare array. `PostList`, `PageList`, `PostTagList`, and `PostCategoryListResponse` all carry `pageInfo`, whose shape is [`PageInfo`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/apollo/commonTypeDefs.ts#L35-L41): `hasNextPage`, `hasPreviousPage`, `startCursor`, `endCursor`. Feed `endCursor` back in as `cursor` to get the next page.

Operations ending in a bare list (`cpPosts`, `cpPages`, `cpMenus`, `cpCmsMenuList`, `cpMostViewedPosts`, `cpCustomPostTypes`, `cpCustomFieldGroups`) return an array with no `totalCount`, even when the schema accepts the shared cursor arguments.

The CMS list helper recognizes lowercase `sortDirection: "asc"`; other values select descending order. An explicit `orderBy` takes precedence over `sortField`. See the [pagination implementation](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/utils/mongo/mongoose-utils.ts) and [CMS base resolvers](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/utils/base-resolvers.ts).

## Posts

### `cpPostList`: paginated post list

The workhorse for blog indexes, category archives, and search results.

```graphql
query CpPostList(
  $type: String
  $featured: Boolean
  $categoryIds: [String]
  $tagIds: [String]
  $searchValue: String
  $status: PostStatus
  $authorId: String
  $language: String
  $webId: String
  $dateField: PostDateField
  $dateFrom: Date
  $dateTo: Date
  $sortField: String
  $sortDirection: String
  $limit: Int
  $cursor: String
) {
  cpPostList(
    type: $type
    featured: $featured
    categoryIds: $categoryIds
    tagIds: $tagIds
    searchValue: $searchValue
    status: $status
    authorId: $authorId
    language: $language
    webId: $webId
    dateField: $dateField
    dateFrom: $dateFrom
    dateTo: $dateTo
    sortField: $sortField
    sortDirection: $sortDirection
    limit: $limit
    cursor: $cursor
  ) {
    totalCount
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
    posts {
      _id
      title
      slug
      excerpt
      type
      status
      featured
      publishedDate
      createdAt
      updatedAt
      viewCount
      authorKind
      authorId
      categoryIds
      thumbnail {
        url
        name
        type
        size
      }
      customPostType {
        _id
        code
        label
      }
      tags {
        _id
        name
        slug
        colorCode
      }
      categories {
        _id
        name
        slug
      }
    }
  }
}
```

Argument notes, all from [`schemas/posts.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L154-L195):

- `categoryIds` is `[String]`, plural. There is no `categoryId` argument on any post operation.
- `status` is the `PostStatus` enum: `draft`, `published`, `scheduled`, `archived`. Pass `"published"` for public listings.
- `type` filters by post type; combine it with `cpCustomPostTypes` below to build a type-aware archive.
- The schema accepts `dateField`, `dateFrom`, and `dateTo`. Verify date-filter behavior in your installed resolver before depending on it.
- `webId` scopes results to one Web Builder site. Omit it for a standalone CMS.
- `language` requests translated field values. The resolver falls back to the stored values when no translation exists.

Omit `content` from list queries. It is the full HTML body and dominates payload size; fetch it only in the detail operation.

### `cpPost`: single post

Resolves by `_id`, by `slug`, or by `identifier`.

```graphql
query CpPost(
  $id: String
  $slug: String
  $identifier: String
  $count: Int
  $language: String
) {
  cpPost(
    _id: $id
    slug: $slug
    identifier: $identifier
    count: $count
    language: $language
  ) {
    _id
    title
    slug
    excerpt
    content
    type
    status
    featured
    publishedDate
    scheduledDate
    createdAt
    updatedAt
    viewCount
    authorKind
    authorId
    categoryIds
    seoTitle
    seoDescription
    videoUrl
    thumbnail {
      url
      name
      type
      size
    }
    images {
      url
      name
      type
      size
    }
    video {
      url
      name
      type
      size
    }
    documents {
      url
      name
      type
      size
    }
    attachments {
      url
      name
      type
      size
    }
    pdfAttachment {
      pdf {
        url
        name
        type
      }
      pages {
        url
        name
        type
      }
    }
    customFieldsData
    customFieldsMap
    translations {
      _id
      objectId
      language
      title
      excerpt
    }
  }
}
```

`cpPost` increments the view counter when it finds a post. Repeated requests, prefetching, and caching therefore affect the count; it is not a count of unique visitors. See [`increaseViewCount`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts#L523-L549) and the [SSR caching explanation](https://erxes.io/docs/content-plug-cms-next-developers-ssr). The resolver does not filter publication status; request `status` and check it before exposing a record.

The `clientPortalId` argument appears in the schema but the resolver ignores it and always uses the portal from the token. See [`cpPost`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts#L523-L549).

There is no `cpPostDetail`. Older guides and migrated codebases refer to it; the operation is `cpPost`.

### `cpPosts`: list without a pagination envelope

Same arguments as `cpPostList` but returns `[Post]` directly, with no `totalCount` and no `pageInfo`. Use it when you need a fixed, small set (the three latest posts in a sidebar, for example).

```graphql
query CpPosts($type: String, $status: PostStatus, $limit: Int, $sortField: String, $sortDirection: String) {
  cpPosts(
    type: $type
    status: $status
    limit: $limit
    sortField: $sortField
    sortDirection: $sortDirection
  ) {
    _id
    title
    slug
    excerpt
    publishedDate
    thumbnail {
      url
      name
      type
    }
  }
}
```

### `cpPostListWithPagination`: offset pagination

The only CMS operation that accepts `page` and `perPage`. It returns `PostListPagination`, which has `posts` and `totalCount` but **no `pageInfo`**. See [`schemas/posts.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L92-L95).

```graphql
query CpPostListWithPagination(
  $page: Int
  $perPage: Int
  $sortField: String
  $sortDirection: String
  $status: PostStatus
  $categoryIds: [String]
  $type: String
  $language: String
) {
  cpPostListWithPagination(
    page: $page
    perPage: $perPage
    sortField: $sortField
    sortDirection: $sortDirection
    status: $status
    categoryIds: $categoryIds
    type: $type
    language: $language
  ) {
    totalCount
    posts {
      _id
      title
      slug
      excerpt
      publishedDate
      thumbnail {
        url
        name
        type
      }
    }
  }
}
```

Prefer this over cursor pagination when you need numbered page links (`?page=3`). Prefer `cpPostList` when you need infinite scroll.

### `cpMostViewedPosts`: popularity list

`days` is required and must be a positive integer. It is capped at the backend's 30-day view-retention window, and `limit` defaults to 10 and is clamped between 1 and 100. See [`findMostViewedPosts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/posts.ts#L101-L203).

```graphql
query CpMostViewedPosts($days: Int!, $limit: Int, $type: String, $language: String, $webId: String) {
  cpMostViewedPosts(days: $days, limit: $limit, type: $type, language: $language, webId: $webId) {
    _id
    title
    slug
    viewCount
    recentViewCount
    thumbnail {
      url
      name
      type
    }
  }
}
```

`recentViewCount` is the count inside the requested window and is only present on this operation.

## Pages

Pages carry the section list a template renders. `Page.pageItems` is the field that matters for composable layouts; each item has a `type`, an `order`, and a free-form `config` object. See [`schemas/page.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/page.ts#L3-L39).

### `cpPageList`: all pages

```graphql
query CpPageList($language: String, $limit: Int, $cursor: String) {
  cpPageList(language: $language, limit: $limit, cursor: $cursor) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    pages {
      _id
      name
      slug
      description
      coverImage
      type
      status
      createdUserId
      createdAt
      updatedAt
      customFieldsData
      customFieldsMap
    }
  }
}
```

`cpPages(language)` is the unpaginated variant returning `[Page]`. Use it to enumerate routes for `generateStaticParams`.

### `cpCmsPageDetail`: one page with its sections

```graphql
query CpPage($slug: String, $id: String, $language: String) {
  cpCmsPageDetail(slug: $slug, _id: $id, language: $language) {
    _id
    name
    slug
    description
    coverImage
    type
    status
    content
    createdAt
    updatedAt
    customFieldsData
    customFieldsMap
    pageItems {
      _id
      name
      type
      content
      order
      objectType
      objectId
      config
    }
  }
}
```

Pass either `slug` or `_id`; if you pass neither, the resolver returns `null` rather than an error. See [`cpCmsPageDetail`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/page.ts#L121-L138). The portal is applied from the token, so a slug from another portal cannot be read.

`config` is a `JSON` scalar. The GraphQL schema does not describe section-specific fields inside it. Validate that content against the selected template's expected fields at the rendering boundary.

`Page.createdUser` resolves to a `User` from the core subgraph. `createdUserId` is a nullable scalar field that can be selected without those user details. Choose the author information the website needs.

## Categories

### `cpCategories`: category list

Returns `PostCategoryListResponse`, whose array field is named `list`, not `categories`, and not the objects themselves. See [`schemas/category.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/category.ts#L27-L31).

```graphql
query CpCategories(
  $status: CategoryStatus
  $searchValue: String
  $language: String
  $sortField: String
  $sortDirection: String
  $limit: Int
  $cursor: String
) {
  cpCategories(
    status: $status
    searchValue: $searchValue
    language: $language
    sortField: $sortField
    sortDirection: $sortDirection
    limit: $limit
    cursor: $cursor
  ) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    list {
      _id
      name
      slug
      description
      parentId
      status
      createdAt
      updatedAt
      customFieldsData
      parent {
        _id
        name
        slug
      }
    }
  }
}
```

`status` is the `CategoryStatus` enum: `active` or `inactive`. Pass `"active"` for public navigation.

`cpCategories` honours an explicit `clientPortalId` argument, falling back to the token's portal when it is absent: `args.clientPortalId || clientPortal?._id`. See [`cpCategories`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/category.ts#L57-L70). For a website, keep portal selection on the server and do not accept a browser-supplied portal ID.

### `cpCmsCategoryDetail`: one category

```graphql
query CpCategory($slug: String, $id: String, $language: String) {
  cpCmsCategoryDetail(slug: $slug, _id: $id, language: $language) {
    _id
    name
    slug
    description
    parentId
    status
    customFieldsData
    customFieldsMap
  }
}
```

Pair it with `cpPostList(categoryIds: [$id], status: "published")` to render a category archive.

## Menus

Menu items are returned hydrated, so `parent` is populated and the flat list can be assembled into a tree client-side. `kind` is a free-form string; the Web Builder deployer looks for `main` and `footer`. See [`deploy`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/webbuilder/utils/utils.ts#L166-L181).

### `cpMenus`: navigation for a site

```graphql
query CpMenus($kind: String, $language: String, $webId: String) {
  cpMenus(kind: $kind, language: $language, webId: $webId) {
    _id
    parentId
    label
    contentType
    contentTypeId
    type
    linkType
    kind
    icon
    url
    order
    openInNewTab
    target
    linkedContent {
      _id
      title
      slug
      linkType
    }
    parent {
      _id
      label
      url
      order
    }
  }
}
```

The field is `contentTypeId`, lower-case `d`. `contentTypeID` does not exist and is a frequent typo in migrated code. See [`MenuItem`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/menu.ts#L19-L38).

`linkType` is the `MenuLinkType` enum (`URL`, `PAGE`, `POST`, `CATEGORY`, `TAG`) and is what you should branch on when building a link. `linkedContent` gives you the target's `_id`, `title`, and `slug` so you can build the href without a second round trip.

`cpCmsMenuList(clientPortalId, kind, language, ...)` accepts the shared cursor arguments but still returns a bare `[MenuItem]` array; there is no `MenuItemResponse` envelope in the result. Unlike `cpMenus` it ignores `webId`, and its `clientPortalId` argument is also ignored: the resolver always uses the portal from the token. See [`cpCmsMenuList`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/queries/menu.ts#L87-L104).

## Tags and custom post types

### `cpCmsTags`

Returns `PostTagList`; the array field is `tags`.

```graphql
query CpTags($searchValue: String, $language: String, $sortField: String, $sortDirection: String, $limit: Int) {
  cpCmsTags(
    searchValue: $searchValue
    language: $language
    sortField: $sortField
    sortDirection: $sortDirection
    limit: $limit
  ) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    tags {
      _id
      name
      slug
      colorCode
    }
  }
}
```

### `cpCustomPostTypes`

Use this to discover the post `type` values your instance has, then feed one into `cpPostList(type:)`.

```graphql
query CpCustomPostTypes($searchValue: String) {
  cpCustomPostTypes(searchValue: $searchValue) {
    _id
    code
    label
    pluralLabel
    description
    createdAt
  }
}
```

`cpCustomFieldGroups(searchValue, pageId, categoryId, postType, postId)` returns the field-group definitions behind `customFieldsData` and `customFieldsMap`. See [`schemas/customPostType.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/customPostType.ts#L78-L84).

## Post reactions

The schema includes reaction and view-counter mutations. The `cp*` family also contains other writes, so its prefix must not be used as a read-only access rule.

```graphql
mutation CpPostsReact($id: String!, $reaction: PostReactionType!, $action: ReactionModifyType!) {
  cpPostsReact(_id: $id, reaction: $reaction, action: $action) {
    _id
    reactions
    reactionCounts
  }
}
```

`PostReactionType` is `like | love | angry | sad | happy`; `ReactionModifyType` is `inc | dec`. `reactionCounts` is a `JSON` map of reaction to count. `cpPostsIncrementViewCount(_id: String!)` is the explicit counter bump, which you only need if you deliberately avoid `cpPost`. See [`schemas/posts.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L197-L214).

Implement reactions through a separately authorized endpoint with input validation and appropriate request limits. The post-search endpoint in the CSR guide does not accept mutations.

## The `author` field

`Post.author` is declared as `union Author = User`, and `User` comes from the core subgraph, not from `content_api`. See [`schemas/posts.ts:39`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/graphql/schemas/posts.ts#L39).

Two consequences:

- There is no `ClientPortalUser` type in the union. A fragment `... on ClientPortalUser` is a schema validation error. The Client Portal user type in core is `CPUser`, and it is not a member of `Author`.
- Selecting subfields of `author` requires the federated schema to include `User`. Introspect your own gateway before relying on it:

```bash
set -a; source .env.local; set +a
curl -sS "$ERXES_API_URL" \
  -H "content-type: application/json" \
  -H "x-app-token: $ERXES_APP_TOKEN" \
  -d '{"query":"query { __type(name: \"Author\") { kind possibleTypes { name fields { name } } } }"}'
```

`authorKind` and `authorId` can be selected without fetching the federated user's fields. Decide which author information the website should expose; an identifier alone is not a human-readable byline.

## Admin-side operations

Listed for completeness. These need a team-member session or an Apps token and must stay server-side.

### `cmsPostList`

```graphql
query CmsPostList(
  $clientPortalId: String
  $status: PostStatus
  $categoryIds: [String]
  $searchValue: String
  $type: String
  $language: String
  $limit: Int
  $cursor: String
) {
  cmsPostList(
    clientPortalId: $clientPortalId
    status: $status
    categoryIds: $categoryIds
    searchValue: $searchValue
    type: $type
    language: $language
    limit: $limit
    cursor: $cursor
  ) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    posts {
      _id
      title
      slug
      status
      type
      authorKind
      authorId
      createdAt
      updatedAt
      scheduledDate
      publishedDate
    }
  }
}
```

Both `cmsPostList` and `cpPostList` can return unpublished records when no publication filter is applied. Keep preview routes authenticated and enforce the intended publication policy on public endpoints.

### `cmsPages` and `cmsPage`

```graphql
query CmsPages($clientPortalId: String, $searchValue: String, $language: String, $limit: Int) {
  cmsPages(clientPortalId: $clientPortalId, searchValue: $searchValue, language: $language, limit: $limit) {
    _id
    name
    slug
    description
    type
    status
    createdUserId
    createdAt
    updatedAt
  }
}
```

```graphql
query CmsPage($id: String, $slug: String, $clientPortalId: String, $language: String) {
  cmsPage(_id: $id, slug: $slug, clientPortalId: $clientPortalId, language: $language) {
    _id
    name
    slug
    description
    status
    content
    pageItems {
      _id
      name
      type
      order
      objectType
      objectId
      config
    }
  }
}
```

`cmsPage` returns `null` when neither `_id` nor `slug` is supplied.

### `cmsCategories`

```graphql
query CmsCategories($clientPortalId: String, $status: CategoryStatus, $searchValue: String, $language: String, $limit: Int) {
  cmsCategories(
    clientPortalId: $clientPortalId
    status: $status
    searchValue: $searchValue
    language: $language
    limit: $limit
  ) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    list {
      _id
      name
      slug
      parentId
      status
    }
  }
}
```

### `cmsMenuList`

```graphql
query CmsMenuList($clientPortalId: String, $kind: String, $language: String) {
  cmsMenuList(clientPortalId: $clientPortalId, kind: $kind, language: $language) {
    _id
    parentId
    label
    contentType
    contentTypeId
    linkType
    kind
    icon
    url
    order
    openInNewTab
    target
  }
}
```

`clientPortalId` is required at runtime even though the schema marks it nullable.

### `cmsTags` and `contentCMSList`

```graphql
query CmsTags($clientPortalId: String, $searchValue: String, $language: String, $limit: Int) {
  cmsTags(clientPortalId: $clientPortalId, searchValue: $searchValue, language: $language, limit: $limit) {
    totalCount
    tags {
      _id
      name
      slug
      colorCode
    }
  }
}
```

```graphql
query ContentCmsList {
  contentCMSList {
    _id
    name
    description
    clientPortalId
    domain
    publicUrl
    language
    languages
    postUrlField
    postUrlPrefix
    accessPolicy
    allowComments
    defaultPostStatus
  }
}
```

`contentCMSList` is how you discover the `clientPortalId` values a team member can reach. `accessPolicy` is `open` or `assigned`; with `assigned`, only the members in `assignedMemberIds` plus owners can use that CMS. See [`cms-access.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/utils/cms-access.ts#L1-L52).

## Execute a document on the server

Copy the operation you need into a server module and call the `erxesQuery` helper from [Setup](https://erxes.io/docs/content-plug-cms-next-developers-setup):

```ts
import "server-only";
import { erxesQuery } from "@/lib/erxes/server";

const query = `
  query WebsitePostCount {
    cpPostList(status: published, limit: 1) {
      totalCount
    }
  }
`;

export async function getPublishedPostCount() {
  const data = await erxesQuery<{
    cpPostList: { totalCount: number | null } | null;
  }>(query);
  return data.cpPostList?.totalCount ?? 0;
}
```

These documents also work with an appropriately configured GraphQL client. Select only the fields the website renders. Keep server-side operations and credential handling separate from the browser's allowed inputs.

## Social publishing operations

The [Postiz GraphQL module](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/graphql.ts) adds CMS-side operations for a separately configured social-publishing integration:

- `cmsPostizOptions(clientPortalId: String!, language: String)` returns `enabled`, `canManage`, and channels with `id`, `name`, `provider`, and `usable`.
- `cmsPostizDeliveries(postId: String!, language: String!)` returns up to 50 recent deliveries for the selected post and language.
- `cmsPostizValidate(input: CmsPostizShareInput!)` validates a share request before queuing it.
- `cmsPostizShare(input: CmsPostizShareInput!)` queues deliveries and returns their records.
- `cmsPostizRetry(id: String!, reviewedInPostiz: Boolean!)` requests a retry through the service's state checks.
- `cmsPostizEnable(clientPortalId: String!, language: String!, enabled: Boolean!)` changes sharing enablement through the external integration.

For an authenticated administration screen, inspect available channels before offering a share action:

```graphql
query CmsSocialPublishingOptions($clientPortalId: String!, $language: String) {
  cmsPostizOptions(clientPortalId: $clientPortalId, language: $language) {
    enabled
    canManage
    channels {
      id
      name
      provider
      usable
    }
  }
}
```

The [input validator](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/service.ts) requires `postId`, a UUID `requestId`, `language`, one to ten `channelIds`, a nonempty caption of at most 2,800 characters, and a `media` array of up to four URLs. Selected media must already belong to the post's thumbnail or images. The backend appends the public article URL to the caption and validates each selected channel through the bridge.

Only ordinary posts with `status: published` are shareable. The [URL builder](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/content_api/src/modules/cms/postiz/content.ts) uses the CMS public URL or domain, its post URL prefix, and its configured `_id`, `slug`, or `count` identifier. Configure these to match your website before sharing; a valid CMS record alone does not establish a working public article URL.

These operations require administrative access and a running `agent` service providing `postizCms.execute`. This repository has the Content-side bridge, not that service implementation. A queued delivery is not proof of publication; inspect its delivery state and returned URL. Do not expose these operations through the public website endpoint.

## Troubleshooting

- **HTTP 401/403 or "forbidden" GraphQL errors**: the credential is missing, invalid, or the wrong family. Public content reads need the Client Portal token in `x-app-token`; `cms*` operations need a team-member session or an Apps token in `erxes-app-token`.
- **A successful query returns an empty list**: the token matched a portal, but the records belong to a different `clientPortalId`. Confirm the portal association of your posts, pages, and menus; do not fix it by passing a browser-supplied `clientPortalId`.
- **`Unknown argument "page"` / `"perPage"`**: only `cpPostListWithPagination` uses offset pagination. Every other list takes `limit`/`cursor`.
- **`Cannot return null for non-nullable` on `clientPortalId`**: some CMS records require a portal association; audit the data rather than removing the field from the selection set.

