Appearance
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.
| Field | Required | Notes |
|---|---|---|
localId | yes | Stable, unique within the template |
kind | yes | MAIN, COVER, TEXT, INSERT, OUTSERT, DUST_JACKET, ENDPAPER, CONTENT, OTHER |
name | yes | Display name |
defaultQuantity | no | Default 1 |
minQuantity / maxQuantity / quantityStep | no | Storefront quantity validation |
totalPages | no | Default page count (overridable via PAGE_COUNT config dimension) |
mediaSelectors | no | Tenant-agnostic substrate constraints — see "Substrate selection" |
sides, spreadType | no | ONE_SIDED / TWO_SIDED_HEAD_TO_HEAD / TWO_SIDED_HEAD_TO_FOOT; SINGLE_PAGE / READER_SPREAD / PRINTER_SPREAD |
finishedDimensions, colors, layout, finishing | no | Geometry + ink + bleed + finishing defaults |
Operations
Operations live in a top-level operations array (not nested under components), so cross-component dependencies work.
| Field | Required | Notes |
|---|---|---|
localId | yes | Unique across all operations |
componentLocalId | no | Links to a component; null for job-level operations |
kind | yes | PRINTING, OFFSET_PRINTING, DIGITAL_PRINTING, FOLDING, BINDING, SADDLE_STITCHING, CUTTING, LAMINATING, FOIL_STAMPING, EMBOSSING, CASE_MAKING, CASING_IN, SEWING, … |
name | yes | Display name |
defaultParameters | no | JSON string validated against the kind's parameter schema (binding style, fold type, film type, …) |
dependsOn | yes | Array 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.mediaSelectorskeeps 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 instantiationx-applies-to— array of component or operationlocalIds the option targets
Dimension catalogue
| Dimension | Property type | Targets | Effect | Extra |
|---|---|---|---|---|
FINISHED_SIZE | string + enum | component localIds | Sets layout size on each target | x-enum-values required with {width, height, unit} per enum value |
OPTIONAL_COMPONENT | boolean | component localIds | When false, the component (and its operations) is excluded from the job | type must be "boolean" |
OPTIONAL_FINISHING | boolean | operation localIds | When false, the operation is excluded | type must be "boolean" |
BINDING_METHOD | string + enum | operation localIds | Sets bindingStyle on the operation's defaultParameters | — |
COLOR_SPEC | string + enum | component localIds | Sets the colour spec on the component | — |
PAGE_COUNT | integer | component localIds | Writes totalPages on each target | minimum, maximum, multipleOf for client-side validation |
Vendor extensions (Set B — ProductTemplate-only)
| Extension | Required for | Description |
|---|---|---|
x-dimension | every property | One of the six dimension types above. |
x-applies-to | every property | Non-empty array of component/operation localIds. Validated to exist. |
x-enum-labels | FINISHED_SIZE, BINDING_METHOD, COLOR_SPEC | Object mapping enum value → display string. Always provide for storefront UIs. |
x-enum-values | FINISHED_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 asx-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
upsertProductTemplatealways leaves the templateDRAFTand bumpsversion(yes, even on repeated draft saves —publishedVersionis the only number that matters for reproducibility).publishProductTemplatesnapshots the current state as an immutableProductTemplateVersionand flips status toPUBLISHED.instantiateProductTemplatealways uses the latest published version. There is currently no way to pin instantiation to a historical version.- Use
listProductTemplateVersions(templateId)andgetProductTemplateVersion(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 }
}
}configis aJSONscalar — pass it as an inline object literal, not a string. It's validated against the publishedconfigSchema.selectorChoicesresolves substrate per component. A single tenant-library match writes the component'sdesiredSubstrateId; zero or multiple matches leave it unset for the operator/workflow to resolve.- The mutation creates
Component,Operation, andOperationEdgerecords on the job and stampssource_template_id+source_template_versionon the job.
ConfigSchema validation reference
The validator rejects (in addition to standard JSON Schema invariants):
| Path | Condition |
|---|---|
configSchema | Not a JSON object |
configSchema.type | Not "object" |
configSchema.properties.<prop>.x-dimension | Missing or invalid value |
configSchema.properties.<prop>.x-applies-to | Missing, empty, or references unknown localId |
configSchema.properties.<prop>.type | Not "boolean" for OPTIONAL_COMPONENT / OPTIONAL_FINISHING |
configSchema.properties.<prop>.x-enum-values | Missing for FINISHED_SIZE |
configSchema.properties.<prop>.x-enum-values.<val> | Missing width or height |
components[i].mediaSelectors | Empty array (use null instead) |
components[i].mediaSelectors | Duplicate selector localIds |
components[i].mediaSelectors[j] | No constraints set — would match every substrate |
components[i].mediaSelectors[j].weight | Range with min > max |
Permissions
| Permission | Required for |
|---|---|
productTemplate:read | getProductTemplate, listProductTemplateVersions, getProductTemplateVersion |
productTemplate:list | listProductTemplates |
productTemplate:write | upsertProductTemplate, publishProductTemplate |
productTemplate:delete | deleteProductTemplate |
productTemplate:execute | instantiateProductTemplate |
Discovery
- Filter
listProductTemplatesbysku(exact),category(exact),isActive,status("DRAFT"/"PUBLISHED"). - Templates carry free-form
tags(string array) and a singlecategoryfor organisation. UseisActive: falseto 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.