Skip to content

Core patterns ​

These patterns are the same for every domain in the API. Once you know them for jobs, you know them for orders, components, devices and the rest.

IDs ​

Every record has a typed ID made of a short prefix and a sortable unique suffix:

TypePrefixExample
Jobjob_job_01M0R249JMETNBM1Z7466MXGZD
Componentcmp_cmp_01M0R249NQEQGR8E91ZHP09KP0
Operationopr_
Milestonemls_
Customercus_cus_01M0R2496SFANSZFKWPYW17V0J
Orderord_
Order lineoln_
Estimateest_
Invoiceinv_
Shipmentshp_
Filefil_
Devicedev_

Arguments are typed too: getJob(id:) takes a JobID!, and a value that is not a valid job ID (a malformed ID, or an ID with another prefix such as cus_) is rejected before the query runs. Treat IDs as opaque strings and store them as they are.

To find a record by the ID it has in your own system, set externalId when you create it and look it up with get<Entity>By(externalId:):

graphql
query JobByExternalId {
  getJobBy(externalId: "MIS-2026-0042") {
    id
    name
    status
  }
}

get<Entity> and get<Entity>By return null when nothing matches; they do not raise an error.

Pagination ​

List queries return a Relay-style connection:

graphql
query FirstPage {
  listJobs(first: 25) {
    totalCount
    edges {
      cursor
      node {
        id
        name
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
  • first sets the page size. The default is 25; a value above 100 is capped at 100.
  • To get the next page, pass the previous page's pageInfo.endCursor as after. Stop when hasNextPage is false.
  • last and before page backwards on the queries that accept them (for example listJobs, listComponents, listCustomers). Some lists, such as listOrders, take only first and after; the API reference shows the arguments of each query.
  • A cursor encodes the position under the current sort order. Keep the same filter and sort while you page through a result.
graphql
query NextPage($after: String) {
  listJobs(first: 25, after: $after, sort: [{ field: CREATED_AT, direction: DESC }]) {
    edges {
      node {
        id
        name
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Relations on a single record (JobState.components, OrderState.orderLines) are plain lists, not connections, and take no arguments.

Filtering ​

Most list queries take a filter input with one entry per filterable field. Each entry is an operator object. The operators depend on the field type:

Filter typeOperators
StringFiltereq, neq, contains, startsWith, endsWith, in, notIn
IntFiltereq, neq, gt, gte, lt, lte, in, notIn
DateFiltereq, neq, gt, gte, lt, lte
IDFiltereq, neq, in, notIn

contains, startsWith and endsWith ignore case. Dates are ISO 8601 strings. When you set several fields, a record must match all of them.

graphql
query ActiveJobsDueThisWeek {
  listJobs(
    first: 50
    filter: {
      status: { in: ["PREPRESS", "PRESS", "POSTPRESS"] }
      dueAt: { gte: "2026-09-21T00:00:00Z", lt: "2026-09-28T00:00:00Z" }
      name: { contains: "brochure" }
    }
  ) {
    totalCount
    edges {
      node {
        id
        name
        status
        dueAt
      }
    }
  }
}

Points to watch:

  • Status and kind filters are strings. Fields like JobFilterInput.status are StringFilters, so you write status: { eq: "PRESS" } with the value in quotes, even though JobState.status itself is an enum.
  • ID filters always take an operator object. Write billingCustomerId: { eq: "cus_…" }, never billingCustomerId: "cus_…".
  • Only the fields listed on the filter input exist. There are no nested or relationship filters (you cannot filter jobs by a property of their customer) and no and/or/not. Filter by the related record's ID instead, or make two queries. Check the filter input in the reference, for example JobFilterInput.
graphql
query JobsForCustomer {
  listJobs(filter: { billingCustomerId: { eq: "cus_01M0R2496SFANSZFKWPYW17V0J" } }) {
    edges {
      node {
        id
        name
      }
    }
  }
}

Counting and aggregates ​

Jobs, components, operations and metric points have an aggregate query (aggregateJobs, aggregateComponents, …) that takes the same filter and returns counts and numeric summaries without fetching records:

graphql
query OpenJobCount {
  aggregateJobs(filter: { status: { notIn: ["COMPLETED", "CANCELLED"] } }) {
    count
  }
}

Sorting ​

sort is a list of { field, direction } objects, applied in order. direction is ASC or DESC (SortDirection). The sortable fields are an enum per entity, for example JobSortField:

graphql
query JobsByPriority {
  listJobs(
    first: 20
    sort: [{ field: PRIORITY, direction: DESC }, { field: DUE_AT, direction: ASC }]
  ) {
    edges {
      node {
        id
        name
        priority
        dueAt
      }
    }
  }
}

Enums ​

Enum values are upper case on the wire: CONFIRMED, DIGITAL_PRINTING, MM. Use them bare in queries and as JSON strings in variables.

The API reference lists the values of each enum, for example JobStatus. GET /graphql/schema on your tenant host returns the values of the release your tenant runs.

Dates and amounts ​

Timestamps use the ISO8601 scalar. Any valid ISO 8601 offset is accepted on input (2026-10-15T18:00:00+02:00); responses are always in UTC with a Z suffix. Money amounts are plain numbers, next to a currency field holding a three-letter ISO 4217 code such as "EUR".

Mutations ​

Most mutations follow one naming scheme (the API reference shows the exceptions):

MutationArgumentsBehaviour
create<Entity>inputCreates a record.
update<Entity>id, inputChanges the fields you pass and leaves the rest as they are.
upsert<Entity>inputUpdates the record whose id is in the input, or creates one when there is no id.
delete<Entity>varies per entityDeletes the record. Most return { success errors }.

Create, update and upsert return a payload with the record under its own name (job, component, order, …) and an errors list:

graphql
mutation CreateJob {
  createJob(input: { name: "Spring catalogue", quantity: 5000, externalId: "MIS-2026-0042" }) {
    job {
      id
      status
    }
    errors {
      path
      message
    }
  }
}

Always select errors. If the input fails validation, the record is null and errors explains why:

json
{
  "data": {
    "createJob": {
      "job": null,
      "errors": [{ "path": "name", "message": "cannot be empty" }]
    }
  }
}

path names the input field that failed (a dotted path for nested input), and message is a human-readable reason. errors is an empty list on success.

Use variables rather than building query strings:

graphql
mutation UpdateJob($id: JobID!, $input: UpdateJobInput!) {
  updateJob(id: $id, input: $input) {
    job {
      id
      status
      dueAt
    }
    errors {
      path
      message
    }
  }
}
json
{
  "id": "job_01M0R249JMETNBM1Z7466MXGZD",
  "input": { "status": "PREPRESS", "dueAt": "2026-10-01T12:00:00Z" }
}

Errors ​

There are two places an error can appear:

WhereWhat it meansExamples
errors in a mutation payloadThe request ran, but the input was rejected.A required field is blank, a value is out of range.
Top-level errors in the GraphQL responseThe operation did not run as asked.Invalid query or argument (such as a malformed ID), Authentication required, Access denied: not authorized to perform '<action>' on '<resource>'.

A missing record is neither: get<Entity> returns null.

Handle both in client code:

js
const res = await fetch(`https://${host}/graphql`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ query, variables }),
})
const body = await res.json()
if (body.errors?.length) throw new Error(body.errors.map(e => e.message).join('; '))
const { job, errors } = body.data.createJob
if (errors.length) {
  // show errors[i].path / errors[i].message to the user
}

Fragments ​

Fragments name a selection so you can reuse it:

graphql
fragment JobSummary on JobState {
  id
  jobNumber
  name
  status
  dueAt
}

query Dashboard {
  urgent: listJobs(first: 10, sort: [{ field: PRIORITY, direction: DESC }]) {
    edges {
      node {
        ...JobSummary
      }
    }
  }
  dueSoon: listJobs(first: 10, sort: [{ field: DUE_AT, direction: ASC }]) {
    edges {
      node {
        ...JobSummary
      }
    }
  }
}

Good habits ​

  • Select only the fields you use, and keep nesting shallow. Every nested list is fetched in full.
  • Page with first/after instead of pulling large lists in one request.
  • Set externalId on records you create from another system. Before creating, look the record up with get<Entity>By(externalId:) and update it if it exists, so a retried sync does not create duplicates.
  • Use IDs as cache keys; they never change.

Connect machines, automate workflows, and build custom apps for print production.