GraphQL AI Text Mutations

PagibleAI adds authenticated GraphQL mutations for generating text, refining structured page content, describing stored files, transcribing audio and translating text. The available operations depend on the configured AI providers and the current user’s CMS permissions.

See PagibleAI GraphQL API for authentication and JSON scalar handling, GraphQL Pages for conflict-safe drafts, and GraphQL Files for stored file references and multipart uploads.

Generate text with write

write generates plain text from a prompt. You can add context and up to ten stored file UUIDs as reference material.

mutation WriteText(
  $prompt: String!
  $context: String
  $files: [String!]
) {
  write(prompt: $prompt, context: $context, files: $files)
}
{
  "prompt": "Write a concise product description for wireless headphones.",
  "context": "Audience: commuters. Tone: direct and practical.",
  "files": ["018f2567-8508-7a62-9309-41708fbfe5fa"]
}

write arguments

Argument
Type
Limit
prompt
String!
Required, at most 2,000 characters
context
String
At most 30,000 characters
files
[String!]
At most 10 file UUIDs; each at most 36 characters

The mutation returns String and requires the text:write permission. Referenced files also require file:view permission.

Refine structured content

refine rewrites existing CMS content and validates the result against the selected content schema. Encode the original content as a JSON string for the MLL JSON scalar, describe the desired changes, and use pagetype when the page type has a specialized schema. The caller needs the page:refine permission.

mutation RefineContent(
  $prompt: String!
  $content: JSON!
  $type: String
  $context: String
  $lang: String
  $pagetype: String
) {
  refine(
    prompt: $prompt
    content: $content
    type: $type
    context: $context
    lang: $lang
    pagetype: $pagetype
  )
}
{
  "prompt": "Make the introduction clearer and add a specific call to action.",
  "content": "[{\"id\":\"intro\",\"type\":\"text\",\"group\":\"main\",\"data\":{\"text\":\"Take a look at our services.\"}}]",
  "type": "content",
  "context": "B2B consulting landing page",
  "lang": "en",
  "pagetype": "page"
}

refine arguments

Argument
Type
Limit
prompt
String!
Required, at most 2,000 characters
content
JSON!
JSON-encoded existing CMS content
type
String
Schema group, at most 255 characters
context
String
At most 30,000 characters
lang
String
Output language, at most 10 characters
pagetype
String
Page type, at most 255 characters

The mutation returns validated JSON as an encoded string. Parse it before review, then pass the encoded result to savePage when you want to create a draft. refine does not save or publish the page itself.

While refining, the AI model can use CMS tools to read pages, shared elements and files, and to add, save, trash or restore shared elements and files. These tools run with the caller's permissions and only create draft versions; nothing is published. If the provider supports it, the model can also search and fetch web content.

{
  "data": {
    "refine": "[{\"id\":\"intro\",\"type\":\"text\",\"group\":\"main\",\"data\":{\"text\":\"Choose the service that fits your next project, then book a consultation.\"}}]"
  }
}

Save refined content as a page draft

mutation SaveRefinedPage(
  $page: ID!
  $latestId: ID!
  $content: JSON!
) {
  savePage(
    id: $page
    latestId: $latestId
    input: {content: $content}
  ) {
    id
    changed
    latest { id published }
  }
}
const refined = JSON.parse(refineResult.data.refine);

// Validate and render a preview before saving.
const saveVariables = {
  page: page.id,
  latestId: page.latest.id,
  content: JSON.stringify(refined),
};

const saveResult = await runGraphql(SAVE_REFINED_PAGE, saveVariables);

if (saveResult.data.savePage.changed) {
  // Re-read the page and review the conflict before continuing.
}

// savePage created a draft. It did not publish the page.

Treat generated content as untrusted editorial input: validate it against the active theme schema, preview it, inspect links and factual claims, and only then save the draft. Publishing remains a separate operation.

Describe a stored file

mutation DescribeFile($file: String!, $lang: String) {
  describe(file: $file, lang: $lang)
}
{
  "file": "018f2567-8508-7a62-9309-41708fbfe5fa",
  "lang": "en"
}
{
  "data": {
    "describe": "A side view of a compact electric bicycle beside a city café."
  }
}

file is a stored file UUID of at most 36 characters; lang is at most 5 characters. The mutation returns a text description and requires both file:describe and file:view.

Transcribe uploaded audio

transcribe accepts an Upload! using the GraphQL multipart request specification. It returns the transcription through the JSON scalar as a JSON-encoded list of segments. Each segment contains start and end times formatted as HH:MM:SS.mmm and the spoken text.

mutation TranscribeAudio($file: Upload!) {
  transcribe(file: $file)
}
{
  "data": {
    "transcribe": "[{\"start\":\"00:00:00.000\",\"end\":\"00:00:02.480\",\"text\":\"Welcome to the product briefing.\"}]"
  }
}

The upload must comply with the configured file-size and MIME policies. The caller needs audio:transcribe permission.

Translate text

mutation TranslateText(
  $texts: [String!]!
  $to: String!
  $from: String
  $context: String
) {
  translate(texts: $texts, to: $to, from: $from, context: $context)
}
{
  "texts": [
    "Welcome to our website.",
    "Compare our service plans."
  ],
  "to": "de",
  "from": "en",
  "context": "Formal business website"
}
{
  "data": {
    "translate": [
      "Willkommen auf unserer Website.",
      "Vergleichen Sie unsere Servicepakete."
    ]
  }
}

translate arguments

Argument
Type
Limit
texts
[String!]!
1 to 50 texts; each at most 10,000 characters
to
String!
Target language code, at most 5 characters
from
String
Source language code, at most 5 characters
context
String
At most 1,000 characters

The result is [String!]! in the same order as the input. The caller needs text:translate permission.

Errors and provider configuration

AI calls can fail because of validation, permissions, provider configuration, rate limits or upstream service errors. Check the GraphQL errors array and keep provider credentials in server-side configuration. These mutations return generated data; they do not publish CMS content automatically.

All AI mutations share the cms-ai rate limiter, which allows 10 requests per minute per user (or per IP address for guests). Further requests are rejected until the window resets.

Stored files that point to remote URLs are fetched for write and describe. URLs resolving to private or reserved IP addresses are rejected unless CMS_ALLOW_INTERNAL=true is set.

When the AI provider rejects a request, errors[].message contains the provider's own error message, for example an invalid API key, an exhausted quota or an unsupported model. The full error is also written to the Laravel log.

{
  "errors": [
    {
      "message": "Incorrect API key provided.",
      "path": ["write"]
    }
  ],
  "data": {
    "write": null
  }
}

Troubleshoot AI text mutations

Common AI text problems

Symptom
Likely cause
Resolution
Prompt must not be empty
The prompt is blank
Send a specific instruction within the documented limit
Insufficient permissions
The mutation or a referenced file is not allowed
Check the AI capability and file:view
Rate limit exceeded
More than 10 AI requests per minute
Wait for the window to reset and queue or batch requests
Invalid JSON value
refine content was sent as a native array
Encode the content once for the JSON scalar
Content exceeds a limit
Input bytes or nesting are above configured AI limits
Reduce the content or split the operation
Invalid content in refine response
The provider output failed schema validation
Clarify the prompt or use a compatible model
Remote file rejected
The file URL resolves to a private or reserved IP address
Use a public URL or set CMS_ALLOW_INTERNAL=true for trusted internal hosts
Provider error message in errors[].message
Credentials, model, quota or upstream service failed
Read the provider message, then check server-side AI configuration and the Laravel log
Client timed out
The client timeout is shorter than the AI operation
Align it with the configured CMS AI timeout
Public page did not change
Generated output was returned but not saved and published
Review, savePage, then publish separately