Skip to main content

Drafts

Endpoints for the Template draft workflow. A draft lets you edit a Template safely without touching the published version: start editing to get a draft copy, change its files, processors, inputs, and settings, then either commit the draft onto the original or discard it.

A draft is itself a Template row whose editingId points back at the original. Only one draft can exist per Template at a time. Creating a Template already provisions a draft on it (the create handler calls template.createDraft()), so a freshly-created Template has a draft immediately and starting to edit returns that existing draft rather than making a second one.

Every route on this page is gated by the templates license feature (template.ensureTemplates), and the write routes require permission.template.write on the original Template.

Draft Workflow Overview

  1. Start editing - POST /api/templates/{id}/_edit returns the draft copy (creating one only if none exists).
  2. Edit the draft - modify its files, processors, inputs, and settings.
  3. Commit the draft - POST /api/templates/{id}/draft/_commit applies the changes to the original and deletes the draft.
  4. Discard the draft - DELETE /api/templates/{id}/draft throws the changes away and leaves the original untouched.
The {id} is always the original Template id

On the draft, commit, and delete routes, {id} is the original Template's id. The route resolves the original (template.lookup), then looks up its draft (template.draft(template.id)). You do not pass the draft's own id in the path.


POST /api/templates/{id}/_edit

Start editing a Template: return its draft copy, creating one only if none exists yet. Because creating a Template already provisions a draft, this usually returns the existing draft (only one draft can exist per Template).

Authentication: Required (session)

Pre-blocks: template.lookup(params.id), permission.template.write(pre.template)

Response:

Responds 200 with the draft object directly (the handler does not use .created(), so the status is 200, not 201). The draft is a Template row whose editingId is the original Template's id, and it links its commit endpoint via inf:commit-draft. Its inputs, processors, and files arrays carry the copies made from the original.

Start editing a template (create a draft)POST /api/templates/00000000-0000-4000-8000-000000000001/_edit
POST /api/templates/{id}/_edit returns a draft copy of the template, creating one if none exists or returning the existing draft otherwise (only one draft per template). The draft is a Template row with `editingId` pointing back at the original. Requires `permission.template.write` and the `templates` feature. Returns 200 with the draft object (NOT 201).
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:template-takeover": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/_takeover"
},
"inf:template-share": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/shares/{principalId}",
"templated": true
},
"inf:template-processors": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/processors"
},
"inf:template-inputs": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/inputs"
},
"inf:template-owner": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/owner"
},
"inf:template-shares": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/shares"
},
"inf:template-files": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/files"
},
"inf:template-file-upload": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/files/_upload"
},
"inf:template-file": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/template-file"
},
"inf:export": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/_export"
},
"inf:render": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/_render"
},
"inf:render-prompt": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/_render-prompt"
},
"inf:template-copy": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/_copy"
},
"inf:processor-order": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/_processor-order"
},
"inf:user-settings": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/settings"
},
"inf:template-datasets": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/datasets"
},
"inf:input-order": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/_input-order"
},
"inf:favorite": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/favorite"
},
"inf:template-input-locks": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/_edit/input-locks"
}
},
"naturalId": "admin:monthly-sales-report-2",
"permissions": {
"assignTags": true,
"edit": true,
"share": true,
"delete": true,
"write": true,
"changeOwner": true,
"copy": true,
"rename": true,
"modifyInputSettings": true
},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"type": "nunjucks",
"name": "Monthly Sales Report",
"ownerId": "admin",
"slug": "monthly-sales-report-2",
"description": null,
"settings": {},
"editingId": "00000000-0000-4000-8000-000000000001",
"templateFileId": null,
"letterheadId": null,
"handler": "pdf",
"handlerOptions": {},
"source": null,
"sourceId": null,
"shared": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"files": [],
"inputs": [],
"processors": []
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/templates/{id}/draft

Get the draft for a Template. {id} is the original Template's id.

Authentication: Required (session)

Pre-blocks: template.lookup(params.id), template.draft(pre.template.id)

Response:

Responds 200 with the draft Template. It carries editingId (the original's id) and exposes both inf:original (the published Template) and inf:commit-draft links. Returns 404 Not Found when no draft exists for the Template (the template.draft server method throws not-found).

Get a template draftGET /api/templates/00000000-0000-4000-8000-000000000001/draft
GET /api/templates/{id}/draft returns the draft for the original template id, or 404 when no draft exists. The draft carries `editingId` (the original) and links `inf:original` / `inf:commit-draft`.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:template-takeover": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/_takeover"
},
"inf:template-share": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/shares/{principalId}",
"templated": true
},
"inf:template-processors": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/processors"
},
"inf:template-inputs": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/inputs"
},
"inf:template-owner": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/owner"
},
"inf:template-shares": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/shares"
},
"inf:template-files": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/files"
},
"inf:template-file-upload": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/files/_upload"
},
"inf:template-file": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/template-file"
},
"inf:export": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/_export"
},
"inf:render": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/_render"
},
"inf:render-prompt": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/_render-prompt"
},
"inf:template-copy": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/_copy"
},
"inf:processor-order": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/_processor-order"
},
"inf:user-settings": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/settings"
},
"inf:template-datasets": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/datasets"
},
"inf:input-order": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/_input-order"
},
"inf:favorite": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/favorite"
},
"inf:template-input-locks": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000002/input-locks"
},
"inf:original": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001"
}
},
"naturalId": "admin:quarterly-revenue-report-2",
"permissions": {
"assignTags": true,
"edit": true,
"share": true,
"delete": true,
"write": true,
"changeOwner": true,
"copy": true,
"rename": true,
"modifyInputSettings": true
},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000002",
"type": "nunjucks",
"name": "Quarterly Revenue Report",
"ownerId": "admin",
"slug": "quarterly-revenue-report-2",
"description": null,
"settings": {},
"editingId": "00000000-0000-4000-8000-000000000001",
"templateFileId": null,
"letterheadId": null,
"handler": "pdf",
"handlerOptions": {},
"source": null,
"sourceId": null,
"shared": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"files": [],
"inputs": [],
"processors": []
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/templates/{id}/draft/_commit

Commit a draft: apply its files, processors, inputs, and settings onto the original Template, then delete the draft. {id} is the original Template's id.

Authentication: Required (session)

Pre-blocks: template.lookup(params.id), permission.template.write(pre.template), template.draft(pre.template.id)

Response:

Responds 200 with the original Template (its editingId is null again).

Commit a draftPOST /api/templates/00000000-0000-4000-8000-000000000001/draft/_commit
POST /api/templates/{id}/draft/_commit applies the draft’s files/processors/inputs/settings onto the original template, deletes the draft, and writes a `templateUpdated` activity. {id} is the ORIGINAL template id. Returns 200 with the original template (its `editingId` is null again).
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:draft": {
"href": "https://informer.example.com/api/templates/admin%3Ainventory-summary/draft"
},
"inf:edit": {
"href": "https://informer.example.com/api/templates/admin%3Ainventory-summary/_edit"
},
"inf:comments": {
"href": "https://informer.example.com/api/templates/admin%3Ainventory-summary/comments{?sort,limit}",
"templated": true
},
"inf:template-share": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/shares/{principalId}",
"templated": true
},
"inf:template-processors": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/processors"
},
"inf:template-inputs": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/inputs"
},
"inf:template-owner": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/owner"
},
"inf:template-shares": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/shares"
},
"inf:template-files": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/files"
},
"inf:template-file-upload": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/files/_upload"
},
"inf:template-file": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/template-file"
},
"inf:export": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/_export"
},
"inf:render": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/_render"
},
"inf:render-prompt": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/_render-prompt"
},
"inf:template-copy": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/_copy"
},
"inf:processor-order": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/_processor-order"
},
"inf:user-settings": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/settings"
},
"inf:template-datasets": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/datasets"
},
"inf:input-order": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/_input-order"
},
"inf:favorite": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/favorite"
},
"inf:template-input-locks": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/draft/_commit/input-locks"
}
},
"naturalId": "admin:inventory-summary",
"permissions": {
"assignTags": true,
"edit": true,
"share": true,
"delete": true,
"write": true,
"changeOwner": true,
"copy": true,
"rename": true,
"modifyInputSettings": true
},
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"type": "nunjucks",
"name": "Inventory Summary",
"ownerId": "admin",
"slug": "inventory-summary",
"description": null,
"settings": {},
"editingId": null,
"templateFileId": null,
"letterheadId": null,
"handler": "pdf",
"handlerOptions": {},
"source": null,
"sourceId": null,
"shared": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"files": [],
"inputs": [],
"processors": []
}
Captured from the API examples test suite; ids and timestamps are normalized.

Side Effects:

  • Draft files, processors, inputs, and settings are applied to the original Template.
  • The draft is deleted.
  • A templateUpdated activity is logged.

DELETE /api/templates/{id}/draft

Discard a draft, throwing away all of its changes. {id} is the original Template's id.

Authentication: Required (session)

Pre-blocks: template.lookup(params.id), permission.template.write(pre.template), template.draft(pre.template.id)

Response:

Responds 200 with an empty body (Informer convention via the generic db.remove handler), not 204 No Content.

Discard a draftDELETE /api/templates/00000000-0000-4000-8000-000000000001/draft
DELETE /api/templates/{id}/draft discards the draft (and its files/processors/inputs) via the generic db.remove handler, leaving the original untouched. {id} is the ORIGINAL template id. Returns 200 with an EMPTY body (Informer convention), NOT 204.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

Side Effects:

  • The draft Template and its files, processors, and inputs are deleted.
  • The original Template remains unchanged.

Draft Field Synchronization

When a draft is created, the original Template's editable state is copied onto it: templateFileId, type, settings, letterheadId, handler, and handlerOptions, along with the original's files, processors, and inputs. File references inside settings (for example PDF header/footer file ids) are rewritten to point at the copied draft files.

Editing Workflow Example

// 1. Start editing (returns the draft; only one draft per Template)
POST /api/templates/{id}/_edit
// Returns 200 with the draft, editingId === {id}

// 2. Edit the draft - update a file's contents
PUT /api/templates/{draftId}/files/{fileId}/contents

// 3. Edit the draft - add a processor
POST /api/templates/{draftId}/processors

// 4. Edit the draft - update settings
PUT /api/templates/{draftId}/settings

// 5. Commit the draft onto the original ({id} is the original)
POST /api/templates/{id}/draft/_commit
// Returns 200 with the original Template (editingId === null)

// OR: discard the draft instead ({id} is the original)
DELETE /api/templates/{id}/draft
// Returns 200 with an empty body

Draft Constraints

  • Only one draft per Template at a time.
  • Drafts cannot have their own drafts (no nested drafts).
  • The draft and original share the same owner.
  • Committing or discarding a draft requires permission.template.write on the original Template.
  • All draft routes require the templates license feature.