product-templates / iac-f63ed22f4e50Markdown ↓

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. mediaSelectors keeps 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 instantiation
  • x-applies-to — array of component or operation localIds 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. The x-input-type / x-relatedResource / x-supportsExpression keys 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

  • 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

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 desiredSubstrateId onto component.extensions; 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):

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 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.

Published platform manual · revision d8dcda834fe2