From superops-ai
Provides GraphQL API patterns for SuperOps.ai: Bearer token auth, query/mutation building, cursor pagination, rate limiting, and error handling.
npx claudepluginhub wyre-technology/msp-claude-plugins --plugin superopsThis skill uses the workspace's default tool permissions.
SuperOps.ai uses a GraphQL API for all integrations. Unlike REST APIs, GraphQL allows you to request exactly the data you need in a single request. This skill covers authentication, query patterns, mutations, pagination, rate limiting, and error handling.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Analyzes BMad project state from catalog CSV, configs, artifacts, and query to recommend next skills or answer questions. Useful for help requests, 'what next', or starting BMad.
SuperOps.ai uses a GraphQL API for all integrations. Unlike REST APIs, GraphQL allows you to request exactly the data you need in a single request. This skill covers authentication, query patterns, mutations, pagination, rate limiting, and error handling.
SuperOps.ai uses Bearer token authentication. You need:
POST /msp
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
CustomerSubDomain: yourcompany
Note: You can only have one active API token. Regenerating creates a new token and invalidates the old one.
export SUPEROPS_API_KEY="your-api-token"
export SUPEROPS_SUBDOMAIN="yourcompany"
export SUPEROPS_REGION="us" # or "eu"
SuperOps.ai provides region-specific endpoints:
| Platform | Region | Endpoint |
|---|---|---|
| MSP | US | https://api.superops.ai/msp |
| MSP | EU | https://euapi.superops.ai/msp |
| IT | US | https://api.superops.ai/it |
| IT | EU | https://euapi.superops.ai/it |
{
"query": "query or mutation string",
"variables": {
"variableName": "value"
}
}
query getClientList($input: ListInfoInput!) {
getClientList(input: $input) {
clients {
accountId
name
status
}
listInfo {
totalCount
hasNextPage
endCursor
}
}
}
Variables:
{
"input": {
"first": 50,
"filter": {
"status": "Active"
}
}
}
mutation createTicket($input: CreateTicketInput!) {
createTicket(input: $input) {
ticketId
ticketNumber
subject
status
}
}
Variables:
{
"input": {
"subject": "Issue with email",
"client": {
"accountId": "client-uuid"
},
"priority": "HIGH"
}
}
SuperOps.ai uses cursor-based pagination for large result sets.
| Parameter | Type | Description |
|---|---|---|
first | Int | Number of items to return (max 500) |
after | String | Cursor for next page |
before | String | Cursor for previous page |
last | Int | Number of items from end |
{
"data": {
"getAssetList": {
"assets": [...],
"listInfo": {
"totalCount": 1250,
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjQ5"
}
}
}
}
query getAssetListPaginated($input: ListInfoInput!) {
getAssetList(input: $input) {
assets {
assetId
name
}
listInfo {
totalCount
hasNextPage
endCursor
}
}
}
First page:
{
"input": {
"first": 100
}
}
Next page:
{
"input": {
"first": 100,
"after": "YXJyYXljb25uZWN0aW9uOjk5"
}
}
async function fetchAllAssets(filter = {}) {
const allAssets = [];
let hasNextPage = true;
let cursor = null;
while (hasNextPage) {
const variables = {
input: {
first: 100,
filter,
...(cursor && { after: cursor })
}
};
const response = await graphqlRequest(GET_ASSETS_QUERY, variables);
const { assets, listInfo } = response.data.getAssetList;
allAssets.push(...assets);
hasNextPage = listInfo.hasNextPage;
cursor = listInfo.endCursor;
}
return allAssets;
}
X-RateLimit-Limit: 800
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1708012345
{
"errors": [
{
"message": "Rate limit exceeded. Please retry after 30 seconds.",
"extensions": {
"code": "RATE_LIMITED",
"retryAfter": 30
}
}
]
}
async function requestWithRetry(query, variables, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await graphqlRequest(query, variables);
if (response.errors?.some(e => e.extensions?.code === 'RATE_LIMITED')) {
const retryAfter = response.errors[0].extensions.retryAfter || 30;
const jitter = Math.random() * 1000;
await sleep(retryAfter * 1000 + jitter);
continue;
}
return response;
} catch (error) {
if (attempt === maxRetries - 1) throw error;
// Exponential backoff with jitter
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
await sleep(delay);
}
}
}
| Operator | Description | Example |
|---|---|---|
eq | Equals | { "status": "Active" } |
ne | Not equals | { "status": { "ne": "Closed" } } |
in | In array | { "status": ["Open", "In Progress"] } |
contains | Contains substring | { "name": { "contains": "Acme" } } |
startsWith | Starts with | { "name": { "startsWith": "A" } } |
gt | Greater than | { "createdTime": { "gt": "2024-01-01" } } |
gte | Greater than or equal | { "priority": { "gte": "HIGH" } } |
lt | Less than | { "createdTime": { "lt": "2024-02-01" } } |
lte | Less than or equal | { "count": { "lte": 10 } } |
{
"input": {
"filter": {
"and": [
{ "status": ["Open", "In Progress"] },
{ "priority": { "in": ["Critical", "High"] } },
{
"or": [
{ "client": { "accountId": "client-1" } },
{ "client": { "accountId": "client-2" } }
]
}
]
}
}
}
{
"input": {
"orderBy": {
"field": "createdTime",
"direction": "DESC"
}
}
}
All dates and times must be in UTC with ISO 8601 format:
2024-02-15T10:30:00Z
{
"filter": {
"createdTime": {
"gte": "2024-02-01T00:00:00Z",
"lte": "2024-02-29T23:59:59Z"
}
}
}
// Get tickets from last 7 days
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const variables = {
input: {
filter: {
createdTime: {
gte: sevenDaysAgo.toISOString()
}
}
}
};
{
"data": null,
"errors": [
{
"message": "Client not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["getClient"],
"extensions": {
"code": "NOT_FOUND",
"field": "accountId"
}
}
]
}
| Code | Description | Resolution |
|---|---|---|
UNAUTHENTICATED | Invalid/missing token | Check API token |
FORBIDDEN | Insufficient permissions | Check user role |
NOT_FOUND | Entity doesn't exist | Verify ID |
BAD_REQUEST | Invalid input | Check query/variables |
RATE_LIMITED | Too many requests | Implement backoff |
INTERNAL_ERROR | Server error | Retry with backoff |
async function safeGraphQLRequest(query, variables) {
try {
const response = await graphqlRequest(query, variables);
if (response.errors) {
for (const error of response.errors) {
switch (error.extensions?.code) {
case 'UNAUTHENTICATED':
throw new AuthenticationError(error.message);
case 'FORBIDDEN':
throw new PermissionError(error.message);
case 'NOT_FOUND':
throw new NotFoundError(error.message, error.path);
case 'RATE_LIMITED':
throw new RateLimitError(error.message, error.extensions.retryAfter);
default:
throw new APIError(error.message, error.extensions?.code);
}
}
}
return response.data;
} catch (error) {
// Handle network errors
if (error.code === 'ECONNREFUSED') {
throw new NetworkError('Unable to connect to SuperOps.ai API');
}
throw error;
}
}
In SuperOps.ai GraphQL:
nullnull as input can reset a fieldundefined (don't include field) to leave unchanged// This will CLEAR the assignee
{ "assignee": null }
// This will leave assignee unchanged
{ /* assignee field omitted */ }
# Good - specific fields
query {
getClientList(input: { first: 50 }) {
clients {
accountId
name
status
}
}
}
# Avoid - requesting everything
query {
getClientList(input: { first: 50 }) {
clients {
accountId
name
status
emailDomains
website
phone
industry
# ... many more fields
}
}
}
# Good - reusable with variables
query getClient($id: ID!) {
getClient(input: { accountId: $id }) {
name
status
}
}
# Avoid - hardcoded values
query {
getClient(input: { accountId: "abc123" }) {
name
status
}
}
# Good - single request for related data
query getDashboard($clientId: ID!) {
client: getClient(input: { accountId: $clientId }) {
name
}
tickets: getTicketList(input: {
filter: { client: { accountId: $clientId }, status: "Open" }
}) {
listInfo { totalCount }
}
assets: getAssetList(input: {
filter: { client: { accountId: $clientId }, status: "Online" }
}) {
listInfo { totalCount }
}
}
const cache = new Map();
async function getClientList() {
const cacheKey = 'clients';
const cached = cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
const data = await fetchAllClients();
cache.set(cacheKey, {
data,
expires: Date.now() + 5 * 60 * 1000 // 5 minutes
});
return data;
}
const SUPEROPS_API = process.env.SUPEROPS_REGION === 'eu'
? 'https://euapi.superops.ai/msp'
: 'https://api.superops.ai/msp';
async function graphqlRequest(query, variables = {}) {
const response = await fetch(SUPEROPS_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.SUPEROPS_API_KEY}`,
'CustomerSubDomain': process.env.SUPEROPS_SUBDOMAIN
},
body: JSON.stringify({ query, variables })
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
// Usage
const GET_TICKETS = `
query getTicketList($input: ListInfoInput!) {
getTicketList(input: $input) {
tickets {
ticketId
ticketNumber
subject
status
priority
}
listInfo {
totalCount
hasNextPage
}
}
}
`;
const result = await graphqlRequest(GET_TICKETS, {
input: {
first: 50,
filter: { status: ["Open", "In Progress"] }
}
});