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
- Start editing -
POST /api/templates/{id}/_editreturns the draft copy (creating one only if none exists). - Edit the draft - modify its files, processors, inputs, and settings.
- Commit the draft -
POST /api/templates/{id}/draft/_commitapplies the changes to the original and deletes the draft. - Discard the draft -
DELETE /api/templates/{id}/draftthrows the changes away and leaves the original untouched.
{id} is always the original Template idOn 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.
{
"_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": []
}
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).
{
"_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": []
}
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).
{
"_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": []
}
Side Effects:
- Draft files, processors, inputs, and settings are applied to the original Template.
- The draft is deleted.
- A
templateUpdatedactivity 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.
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.writeon the original Template. - All draft routes require the
templateslicense feature.