Skip to content

Product Template Authoring Guide ​

A ProductTemplate is a reusable blueprint for a print product. It defines the components (cover, text block, inserts), the production operations (printing, folding, binding), the dependency graph between them, and — for storefront / Web2Print use — a configSchema exposing customer-facing options.

Templates follow a draft / publish lifecycle: every upsertProductTemplate leaves the template in DRAFT. Only publishProductTemplate cuts an immutable snapshot that can be instantiateProductTemplate'd into a real job.

There is no dedicated tooling for ProductTemplates — everything is driven through the GraphQL API, and the schema is the authority on the exact input and output shape. Use execute_graphql_query / execute_graphql_mutation, and describe_type("UpsertProductTemplateInput"), describe_type("ProductTemplate") and search_schema_mutations("product template") to discover the shapes.

This MCP guide is the LLM-facing condensation.

Top-level shape ​

graphql
mutation {
  upsertProductTemplate(input: {
    name: "A5 Flyer 4/4"
    sku: "FLY-A5-44"
    category: "flyers"
    tags: ["a5", "4/4"]
    components: []   # see below
    operations: []    # see below
    configSchema: {}   # JSON Schema object with x-dimension extensions, see below
  }) {
    productTemplate { id status version }
    errors { path message }
  }
}

Then publish:

graphql
mutation {
  publishProductTemplate(input: { id: "ptl_..." }) {
    productTemplate { id status publishedVersion }
    version
    errors { path message }
  }
}

Components ​

Each component is a physical part of the finished product. Reference them from operations and from configSchema.x-applies-to via their localId.

FieldRequiredNotes
localIdyesStable, unique within the template
kindyesMAIN, COVER, TEXT, INSERT, OUTSERT, DUST_JACKET, ENDPAPER, CONTENT, OTHER
nameyesDisplay name
defaultQuantitynoDefault 1
minQuantity / maxQuantity / quantityStepnoStorefront quantity validation
totalPagesnoDefault page count (overridable via PAGE_COUNT config dimension)
mediaSelectorsnoTenant-agnostic substrate constraints — see "Substrate selection"
sides, spreadTypenoONE_SIDED / TWO_SIDED_HEAD_TO_HEAD / TWO_SIDED_HEAD_TO_FOOT; SINGLE_PAGE / READER_SPREAD / PRINTER_SPREAD
finishedDimensions, colors, layout, finishingnoGeometry + ink + bleed + finishing defaults

Operations ​

Operations live in a top-level operations array (not nested under components), so cross-component dependencies work.

FieldRequiredNotes
localIdyesUnique across all operations
componentLocalIdnoLinks to a component; null for job-level operations
kindyesPRINTING, OFFSET_PRINTING, DIGITAL_PRINTING, FOLDING, BINDING, SADDLE_STITCHING, CUTTING, LAMINATING, FOIL_STAMPING, EMBOSSING, CASE_MAKING, CASING_IN, SEWING, …
nameyesDisplay name
defaultParametersnoJSON string validated against the kind's parameter schema (binding style, fold type, film type, …)
dependsOnyesArray of predecessor localIds — forms the operation DAG; pass [] when there is none

The validator rejects: duplicate localIds, unknown component refs, unknown operation refs in dependsOn, self-references, and cycles.

Substrate selection (mediaSelectors) ​

Templates are tenant-agnostic — never hardcode tenant-specific substrate codes. Each component carries mediaSelectors, which are tenant-neutral constraints (gsm, coatings, stock type, …). The storefront intersects them with the tenant's substrate library; the customer picks one by localId; instantiation re-runs that selector to write desiredSubstrateId onto the component.

text
mediaSelectors: [
  {
    localId: "house_silk"
    name: "Silk text 130gsm"
    weight: { tolerance: { target: 130, tolerance: 20 } }
    coatings: [SATIN, MATTE]
    stockType: [TEXT, BOOK]
  },
  {
    localId: "uncoated"
    name: "Uncoated text 100gsm"
    weight: { range: { min: 90, max: 120 } }
    coatings: [NONE]
  }
]

weight (and thickness) is a @oneOf input: provide exactly one of exact, range, or tolerance. Width / height are deliberately not selectable — parent sheet sizes are tenant-specific.

Each selector must constrain at least one field. Empty arrays are rejected (use null to mean "operator chooses"). localIds must be unique within the component.

Don't put substrate selection in configSchema. It's tenant-dynamic and would force every tenant to fork the template. mediaSelectors keeps templates shareable.

ConfigSchema — customer-facing options ​

configSchema is a JSON Schema (draft-07) object (scalar JSON) on the template input. It drives both validation of the customer's config at instantiation and the Web2Print product-page UI.

Every property must declare two platform-specific vendor extensions:

  • x-dimension — what kind of effect this option has at instantiation
  • x-applies-to — array of component or operation localIds the option targets

Dimension catalogue ​

DimensionProperty typeTargetsEffectExtra
FINISHED_SIZEstring + enumcomponent localIdsSets layout size on each targetx-enum-values required with {width, height, unit} per enum value
OPTIONAL_COMPONENTbooleancomponent localIdsWhen false, the component (and its operations) is excluded from the jobtype must be "boolean"
OPTIONAL_FINISHINGbooleanoperation localIdsWhen false, the operation is excludedtype must be "boolean"
BINDING_METHODstring + enumoperation localIdsSets bindingStyle on the operation's defaultParameters—
COLOR_SPECstring + enumcomponent localIdsSets the colour spec on the component—
PAGE_COUNTintegercomponent localIdsWrites totalPages on each targetminimum, maximum, multipleOf for client-side validation

Vendor extensions (Set B — ProductTemplate-only) ​

ExtensionRequired forDescription
x-dimensionevery propertyOne of the six dimension types above.
x-applies-toevery propertyNon-empty array of component/operation localIds. Validated to exist.
x-enum-labelsFINISHED_SIZE, BINDING_METHOD, COLOR_SPECObject mapping enum value → display string. Always provide for storefront UIs.
x-enum-valuesFINISHED_SIZE (required)Object mapping enum value → { "width": {value, unit}, "height": {value, unit} }.

Different family from integration config_schema.json. The console form-rendering keys such as x-input-type / x-relatedResource / x-supportsExpression (which the integration config-schema validator refuses, see the Integration Builder Guide) are not used here, and are silently ignored by the ProductTemplate validator. The two surfaces have separate validators.

Worked example — hardcover photo book ​

json
{
  "type": "object",
  "properties": {
    "size": {
      "type": "string",
      "title": "Book Size",
      "enum": ["a5", "a4", "square_200"],
      "x-dimension": "FINISHED_SIZE",
      "x-applies-to": ["text", "cover", "endpapers", "dustjacket"],
      "x-enum-labels": {
        "a5": "A5 (148 x 210 mm)",
        "a4": "A4 (210 x 297 mm)",
        "square_200": "Square (200 x 200 mm)"
      },
      "x-enum-values": {
        "a5":         { "width": { "value": 148, "unit": "MM" }, "height": { "value": 210, "unit": "MM" } },
        "a4":         { "width": { "value": 210, "unit": "MM" }, "height": { "value": 297, "unit": "MM" } },
        "square_200": { "width": { "value": 200, "unit": "MM" }, "height": { "value": 200, "unit": "MM" } }
      }
    },
    "dustJacket": {
      "type": "boolean",
      "title": "Include Dust Jacket",
      "default": true,
      "x-dimension": "OPTIONAL_COMPONENT",
      "x-applies-to": ["dustjacket"]
    },
    "lamination": {
      "type": "boolean",
      "title": "Laminate Dust Jacket",
      "default": true,
      "x-dimension": "OPTIONAL_FINISHING",
      "x-applies-to": ["lam_dj"]
    },
    "pages": {
      "type": "integer",
      "title": "Number of Pages",
      "minimum": 8,
      "maximum": 400,
      "multipleOf": 4,
      "x-dimension": "PAGE_COUNT",
      "x-applies-to": ["text"]
    }
  }
}

Lifecycle and versioning ​

  • upsertProductTemplate always leaves the template DRAFT and bumps version (yes, even on repeated draft saves — publishedVersion is the only number that matters for reproducibility).
  • publishProductTemplate snapshots the current state as an immutable ProductTemplateVersion and flips status to PUBLISHED.
  • instantiateProductTemplate always uses the latest published version. There is currently no way to pin instantiation to a historical version.
  • Use listProductTemplateVersions(templateId) and getProductTemplateVersion(templateId, version) to inspect history.

Instantiation ​

graphql
mutation {
  instantiateProductTemplate(input: {
    templateId: "ptl_..."
    jobName: "Hardcover Order"
    quantityOverride: 500
    config: { size: "a4", dustJacket: false, pages: 64 }
    selectorChoices: [
      { componentLocalId: "text", selectorLocalId: "house_silk" }
    ]
  }) {
    job { id name }
    components { id name kind quantity }
    operationCount
    templateVersion
    errors { path message }
  }
}
  • config is a JSON scalar — pass it as an inline object literal, not a string. It's validated against the published configSchema.
  • selectorChoices resolves substrate per component. A single tenant-library match writes the component's desiredSubstrateId; zero or multiple matches leave it unset for the operator/workflow to resolve.
  • The mutation creates Component, Operation, and OperationEdge records on the job and stamps source_template_id + source_template_version on the job.

ConfigSchema validation reference ​

The validator rejects (in addition to standard JSON Schema invariants):

PathCondition
configSchemaNot a JSON object
configSchema.typeNot "object"
configSchema.properties.<prop>.x-dimensionMissing or invalid value
configSchema.properties.<prop>.x-applies-toMissing, empty, or references unknown localId
configSchema.properties.<prop>.typeNot "boolean" for OPTIONAL_COMPONENT / OPTIONAL_FINISHING
configSchema.properties.<prop>.x-enum-valuesMissing for FINISHED_SIZE
configSchema.properties.<prop>.x-enum-values.<val>Missing width or height
components[i].mediaSelectorsEmpty array (use null instead)
components[i].mediaSelectorsDuplicate selector localIds
components[i].mediaSelectors[j]No constraints set — would match every substrate
components[i].mediaSelectors[j].weightRange with min > max

Permissions ​

PermissionRequired for
productTemplate:readgetProductTemplate, listProductTemplateVersions, getProductTemplateVersion
productTemplate:listlistProductTemplates
productTemplate:writeupsertProductTemplate, publishProductTemplate
productTemplate:deletedeleteProductTemplate
productTemplate:executeinstantiateProductTemplate

Discovery ​

  • Filter listProductTemplates by sku (exact), category (exact), isActive, status ("DRAFT" / "PUBLISHED").
  • Templates carry free-form tags (string array) and a single category for organisation. Use isActive: false to hide a template without deleting it.
  • The GraphQL schema is the source of truth for the input shape — this guide condenses, and the schema is always current where a guide can lag.
  • describe_type("UpsertProductTemplateInput") gives you that shape directly.

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