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.
The full long-form reference (every field, every validator, more worked examples)
lives in the repo at
Sources/PrintVertical/Sources/PrintVertical/Domain/ProductTemplate/AUTHORING_GUIDE.md.
The x-* extension catalogue (Set B) below is also covered in
docs/json-schema-extensions.md. This MCP guide is the LLM-facing condensation.
Top-level shape
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: { type: "object", properties: {} }
}) {
productTemplate { id status version }
errors { path message }
}
}Then publish:
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 |
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.
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) JSON object 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 to extensions.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 seven 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. Thex-input-type/x-relatedResource/x-supportsExpressionkeys documented in 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
{
"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
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 writesdesiredSubstrateIdontocomponent.extensions; 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.
Published platform manual · revision d8dcda834fe2