Skip to content

Template Lifecycle — Draft, Review, Publish & Admin API

This page covers the template lifecycle state machine, lifecycle operations, the workflow bridge for approval routing, the resolver chain for template lookup, and the 17 admin endpoints provided by Granit.Templating.Endpoints.

Templates stored in the EF Core store follow a strict lifecycle:

stateDiagram-v2
    [*] --> Draft : SaveDraftAsync
    Draft --> PendingReview : Workflow bridge<br/>(optional)
    PendingReview --> Published : Approve
    PendingReview --> Draft : Reject
    Draft --> Published : PublishAsync<br/>(without Workflow)
    Published --> Archived : New version published<br/>or UnpublishAsync
    Archived --> [*] : Preserved forever<br/>(ISO 27001)
StatusDescriptionDeletable?
DraftBeing edited. Not visible to the rendering pipeline. Only one draft per key.Yes
PendingReviewSubmitted for approval. Only with Granit.Templating.Workflow.No
PublishedActive version. Exactly one per TemplateKey. Resolved by StoreTemplateResolver.No
ArchivedSuperseded by a newer publication. Preserved indefinitely for ISO 27001 audit trail.No
OperationMethodEffect
Save draftIDocumentTemplateStoreWriter.SaveDraftAsyncCreates or replaces the draft for a key
PublishIDocumentTemplateStoreWriter.PublishAsyncPromotes draft to Published, archives previous
UnpublishIDocumentTemplateStoreWriter.UnpublishAsyncArchives the published revision
Delete draftIDocumentTemplateStoreWriter.DeleteDraftAsyncPhysically deletes the draft (drafts only)

Without the Granit.Templating.Workflow package, transitions are direct (Draft -> Published -> Archived) via NullTemplateTransitionHook. A warning is logged at startup when this fallback is active, as it bypasses all permission checks. Installing the Workflow bridge replaces this with WorkflowTemplateTransitionHook, which:

  1. Validates transitions via IWorkflowManager<WorkflowLifecycleStatus>.TransitionAsync — only Completed outcomes allow direct state changes; ApprovalRequested outcomes block direct publish
  2. Supports approval routing (Draft -> PendingReview -> Published)

Templates are resolved by trying resolvers in descending priority order:

PriorityResolverSourcePackage
100StoreTemplateResolverEF Core database (published revisions only)Granit.Templating.EntityFrameworkCore
-100EmbeddedTemplateResolverAssembly embedded resourcesGranit.Templating

Culture fallback strategy (per resolver):

  1. (Name, Culture) — e.g. ("Acme.Invoice", "fr-BE")
  2. (Name, null) — culture-neutral fallback

Tenant scoping is handled inside each resolver. The store-backed resolver looks for a tenant-scoped template first, then falls back to the host-level one.

CultureResource name
Specific ("fr"){AssemblyName}.Templates.{TemplateName}.fr.html
Neutral (fallback){AssemblyName}.Templates.{TemplateName}.html

Register embedded templates:

services.AddEmbeddedTemplates(typeof(AcmeTemplates).Assembly);

17 endpoints are registered via MapGranitTemplating(). All require the Templates.Manage permission (or Templates.Read for GET endpoints).

MethodRouteNameDescription
GET/ListTemplatesPaginated list with filters (status, culture, category, search)
GET/{name}GetTemplateDetailDraft + published detail for a template
POST/CreateTemplateDraftCreate a new template draft
PUT/{name}UpdateTemplateDraftUpdate an existing draft
DELETE/{name}/draftDeleteTemplateDraftDelete draft only (published/archived preserved)
POST/{name}/publishPublishTemplatePublish the current draft
POST/{name}/unpublishUnpublishTemplateArchive the published revision
GET/{name}/lifecycleGetTemplateLifecycleLifecycle status + available transitions
POST/{name}/previewPreviewTemplateRender draft with test data, returns HTML
GET/{name}/variablesGetTemplateVariablesAvailable variables for autocompletion
GET/{name}/historyGetTemplateHistoryPaginated revision history (summaries)
GET/{name}/history/{revisionId}GetTemplateRevisionDetailFull detail of a specific revision
MethodRouteNameDescription
GET/layoutsListAvailableLayoutsDeduplicated list of layout names (code registry + DB)
MethodRouteNameDescription
GET/categoriesListTemplateCategoriesAll categories ordered by sort order
POST/categoriesCreateTemplateCategoryCreate a new category
PUT/categories/{id}UpdateTemplateCategoryUpdate a category
DELETE/categories/{id}DeleteTemplateCategoryDelete a category (409 if templates associated)