Skip to main content

Template Drafts

A draft is a template document that is still being written. You create a draft, change it as many times as you need — replacing the whole document, patching parts of it or applying targeted operations to its tasks and fields — and commit it when it is ready. The commit turns the draft into a template, or into the next version of an existing template.

A draft is stored whether or not its document would save as a template. Every read of a draft and every write to it returns a validation block saying what is still wrong with the document, in the same shape POST /v3/templates/validate uses. This is the difference between a draft and POST /v3/templates, which refuses an invalid document and stores nothing. The commit is the one place on the draft routes where an invalid document is refused. Use a draft when you are building anything larger than a handful of tasks, or when several requests — or several agents — contribute to one template.

Drafts are not visible in the CheckFlow app. They do not appear in the template library, cannot be used to start checklists and send no notifications while they are being edited. Only the commit changes the workspace, in the same way publishing a template in the editor does — see Template Versioning for how versions work.

Who Can See a Draft​

A draft belongs to the workspace, not to the member who started it. Any API key for the workspace can list, read, edit, validate and delete any of the workspace's drafts, whichever member the key acts as. Two routes check the acting member's permissions, because they read or write a template:

  • Creating a draft from a template (fromTemplateKey) reads that template, as exporting it does. A key acting as a Member is refused with 403 FORBIDDEN when the Member's permission on the template's Library folder is Deny.
  • Committing a draft saves a template, as POST /v3/templates and creating a version do. A key acting as a Member needs Template.Creator, and — for a draft seeded from a template — a permission on that template's Library folder other than View or Deny. Without them the commit is refused with 403 FORBIDDEN before anything is queued.

A key acting as an Administrator or as the workspace passes both checks. See Templates for the template permissions in full.

When a draft is committed, the template is written as the key's acting member, and the rest of the team is notified that the template was created or modified, exactly as for POST /v3/templates. The acting member is resolved again when the commit runs, so a commit requested by a member who has been deactivated in the meantime fails. The permissions are asked again then too, so a commit requested by a member who has lost them in the meantime fails with FORBIDDEN.

Draft Lifetime​

A draft expires 30 days after its last write. Every successful PUT, PATCH and operations request moves expiresDateTime 30 days forward. Reading a draft, validating it and polling its commit do not.

To keep a draft alive without changing it, send an empty merge patch — PATCH /v3/drafts/{key} with the body {}.

Once a draft is committed it is kept for seven days from the moment of the commit, as the record of what it became, and then expires. You can delete it sooner.

An expired draft behaves exactly like one that never existed: it is left out of the list and every route answers 404 with the code DRAFT_NOT_FOUND. The error message reminds you that drafts expire, because a draft is the one resource in the API that disappears without anyone deleting it. When you see DRAFT_NOT_FOUND, start a new draft rather than retrying.

Revisions and Concurrency​

Every draft has a revision — a whole number that starts at 1 and goes up by one with each successful write. A list of operations counts as one write however many operations it contains. The same number is sent as the ETag response header (quoted, for example ETag: "4") on every route that returns a whole draft: create, get, replace, patch and operations.

To make a write conditional, send the revision you last read in the If-Match header. If somebody has written to the draft since, the write is refused with 409 Conflict and code CONFLICT, and the message names both revisions — for example you wrote against revision 3 and it is now at revision 4. Read the draft again, reapply your change and retry.

If-Match accepts 4, "4" or the weak form W/"4". The value * is treated as no condition. Anything else — a word, zero, a negative number — is refused with 400 and code VALIDATION_ERROR on field If-Match, rather than ignored.

What happens when you leave If-Match out depends on the route:

RouteWithout If-Match
PUT /v3/drafts/{key}Unconditional. You sent the whole document and it is stored as sent.
PATCH /v3/drafts/{key}Still conditional — on the revision this request read before applying the patch.
POST /v3/drafts/{key}/operationsStill conditional — on the revision this request read before applying the operations.
POST /v3/drafts/{key}/commitUnconditional. The draft is committed as it stands.

A patch or a list of operations only means something against the document it was worked out from, so those two routes never overwrite a change they did not see. Sending If-Match on them is still worth doing: it checks against the revision you read, not the one the request happened to read.

On the MCP server the same revision is passed as the ifRevision argument, because MCP tools have no headers.

Choosing How to Edit​

Three routes change a draft's document. They divide on what you are changing:

You want to changeUseWhy
Everything, and you hold the whole documentPUTWhat you send is what is stored. Anything left out is removed.
The name, description, settings, tags or permissionsPATCHA JSON merge patch changes one member without resending the rest.
Tasks, fields, parameters, conditions or notificationsOperationsAdds, updates, removes and reorders single entries in the document's lists, by ref.
warning

A merge patch replaces arrays whole. Sending "tasks": [...] in a PATCH body leaves the draft with exactly the tasks in your array, whatever it had before — there is no way in a merge patch to change one task of forty. Use the operations route for anything inside a list.

Every write stores the document as the API understood it, not the bytes you sent. The document is read into the template document shape and written out again, so a property the document does not recognise is dropped rather than stored (field properties are the exception — they are kept and reported as violations). Read the document in the response rather than assuming a change landed.

Retries and Rate Limits​

Every POST, PUT, PATCH and DELETE on this page accepts an Idempotency-Key header — see Idempotency. It matters most for the operations route, which is not idempotent on its own: sending the same add_task twice adds two tasks. Sending If-Match protects you too, because the second attempt is refused as stale.

The commit route is safe to retry without a key: a second commit of the same draft reports the first rather than making a second template.

All routes on this page are charged to the standard rate limit budget except GET /v3/drafts/{key}/commit, which is charged to the cheap budget so that polling a commit costs little. See Rate Limits.

Endpoints​

MethodPathDescriptionMCP tool
POST/v3/draftsCreate a draftcreate_template_draft
GET/v3/draftsList draftslist_template_drafts
GET/v3/drafts/{key}Get a draftget_template_draft
PUT/v3/drafts/{key}Replace a draft's documentreplace_template_draft
PATCH/v3/drafts/{key}Patch a draft's documentupdate_template_draft
POST/v3/drafts/{key}/operationsApply operations to a draftapply_draft_operations
POST/v3/drafts/{key}/validateValidate a draftvalidate_template_draft
POST/v3/drafts/{key}/commitCommit a draftcommit_template
GET/v3/drafts/{key}/commitGet commit statusget_commit_status
DELETE/v3/drafts/{key}Delete a draftdelete_template_draft

The Draft Object​

FieldTypeDescription
keystring (GUID)The draft's key. It is the draft's own and never becomes a template's key.
namestringThe document's name, copied so that listing drafts is cheap. Cut to 100 characters here if the document's name is longer; the document keeps it in full. Absent while the document has no name.
revisionintegerWhich write this is, counting from 1. The same value as the ETag header. See Revisions and Concurrency.
basedOnTemplateKeystring (GUID)The template the draft was seeded from with fromTemplateKey. When present, committing writes a new version of this template. Absent for a draft started empty or from a document.
documentobjectThe template document as it now stands. Absent in the list.
validationobjectA Validation object describing what is wrong with the document now. Worked out afresh on every request. Absent in the list.
appliedarrayOnly on the response to the operations route. One Applied entry per operation, in the order sent.
committedTemplateKeystring (GUID)The template the draft became. Absent until the draft is committed.
committedVersionintegerThe version of that template it became. Absent until the draft is committed.
committedDateTimestring (date-time)When it was committed. Absent until the draft is committed.
createdDateTimestring (date-time)When the draft was created.
updatedDateTimestring (date-time)When the draft was last written to.
expiresDateTimestring (date-time)When the draft stops being readable. See Draft Lifetime.
{
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14",
"name": "Invoice Review",
"revision": 2,
"document": {
"name": "Invoice Review",
"description": "Checks a supplier invoice before it is paid.",
"tags": ["q3-audit"],
"permissions": [],
"parameters": [],
"tasks": [
{
"ref": "receive_invoice",
"name": "Receive the invoice",
"isHeading": false,
"halt": "none",
"hiddenByDefault": false,
"assignTo": [],
"assignFrom": [],
"assignedExclusively": false,
"tags": [],
"fields": [
{
"ref": "invoice_number",
"type": "ShortText",
"hiddenByDefault": false,
"label": "Invoice number",
"isRequired": true
}
]
}
],
"conditions": [],
"notifications": []
},
"validation": {
"valid": true,
"errorCount": 0,
"warningCount": 0,
"violations": []
},
"createdDateTime": "2026-09-21T10:15:32.41Z",
"updatedDateTime": "2026-09-21T10:16:08.72Z",
"expiresDateTime": "2026-10-21T10:16:08.72Z"
}

The Validation Object​

The same shape POST /v3/templates/validate returns. A draft that is not valid is still a successful response — read valid, not the status code.

FieldTypeDescription
validbooleantrue when the document has no errors and would save as a template. Warnings do not make it invalid.
errorCountintegerHow many violations have severity error.
warningCountintegerHow many violations have severity warning.
violationsarrayEvery problem found, errors and warnings together. Empty when there are none.

Each violation:

FieldTypeDescription
pathstringWhere in the document the problem is, for example $.tasks[2].fields[0].label.
codestringWhat kind of problem it is, for example property_required or unknown_ref. The codes are listed in Template Documents.
messagestringA sentence describing the problem.
hintstringA suggested fix. Absent when there is none.
severitystringerror or warning.

Validation is worked out on every request rather than stored, because a draft can outlive the things it names — an assignee can leave the team or a linked template can be deleted while the draft sits unchanged. A draft that was valid yesterday can be invalid today.

The Applied Entry​

FieldTypeDescription
opstringThe operation, exactly as sent.
refstringThe ref of the node the operation acted on — the one you named, the one invented for you on an add with no ref, or the new ref after an update that renamed the node. Absent for set_settings and set_permissions, which act on the document rather than on a node.

The Commit Object​

The body of every commit response and every commit status poll. status says which state the commit is in, and the other fields are absent until they have something to say.

FieldTypeDescription
statusstringnotStarted, queued, running, committed or failed. See Commit States.
templateKeystring (GUID)The template the draft committed to, or is going to. For a seeded draft this is the seed template and is present from the start. For any other draft it is absent until the commit finishes, because the key is created while the template is written.
versionintegerThe version the commit wrote — 1 for a new template, or one past the latest version for a seeded draft. This is not necessarily one past the version you seeded from, if somebody published another version in the meantime. Absent until the commit finishes.
isNewTemplatebooleantrue when the draft commits as a new template, false when it commits as a new version of basedOnTemplateKey. Decided by the draft, not by the request.
isReplaybooleantrue when this response reports a commit that this request did not make — a repeated POST or any poll that finds a commit already under way, finished or failed. false on the 202 that started the commit and on notStarted.
queuedDateTimestring (date-time)When the commit was accepted. Use it to decide how long to wait for a commit that stays running.
committedDateTimestring (date-time)When the template was written. Absent until the commit finishes.
refMapobjectEvery ref in the committed document mapped to the GUID key it was written with — tasks, fields, parameters, conditions and notifications in one flat map. Present once status is committed, including on every later poll and repeated POST. Keep it: it is how you find the tasks and fields you built.
errorobjectWhy the commit failed. Present only when status is failed. See below.

error:

FieldTypeDescription
codestringAn API error code, for example TEMPLATE_INVALID, CONFLICT, COMMIT_INTERRUPTED or INTERNAL_ERROR. See Errors.
messagestringA sentence describing the failure.
violationsarrayEverything wrong with the document, in the violation shape. Present only for TEMPLATE_INVALID.
{
"status": "committed",
"templateKey": "9a1d5e7c-2b3f-4c8a-8e6d-1f0b7c4a2d93",
"version": 1,
"isNewTemplate": true,
"isReplay": true,
"queuedDateTime": "2026-09-21T10:18:04.53Z",
"committedDateTime": "2026-09-21T10:18:06.17Z",
"refMap": {
"receive_invoice": "5b7e2c91-8a4f-4d63-b0e2-7c1f9a3d6e58",
"invoice_number": "d2a64f18-3c7b-4e95-8f1a-6b0c2e9d7a34",
"approve_payment": "7f3c1b8e-6d2a-4a57-9e0b-4c8f2d1a6b93",
"decision": "a8e5d2c7-1f4b-4c69-b3d0-9e7a5f2c8b16"
}
}

Commit States​

statusMeaningWhat to do
notStartedNobody has committed this draft. Only a poll returns it.Nothing — this is not an error. Commit when ready.
queuedThis request accepted the commit and queued it. Nothing has been written yet.Poll GET /v3/drafts/{key}/commit.
runningA commit of this draft is already under way — started by an earlier request or another client.Poll. Treat it the same as queued.
committedThe template has been written. templateKey, version and refMap are present.Done.
failedThe commit was accepted and did not finish. error says why.Fix what error names and commit again — except for COMMIT_INTERRUPTED, below.

A commit that fails after it was accepted is recorded on the draft, and the draft stays editable and can be committed again. The usual failures are:

  • TEMPLATE_INVALID — the document was valid when the commit was accepted, but something in the workspace it names changed before the template was written (a tag or member removed, a linked template deleted). error.violations lists the problems. Fix the draft and commit again.
  • CONFLICT — the seed template was deleted or archived before the commit ran, or the draft was edited, deleted or committed by somebody else while this commit was running. In the second case the template was written, and the message names its key and version; the draft is left uncommitted so the other edit is not lost.
  • FORBIDDEN — the acting member lost Template.Creator, or their permission on the seed template's Library folder, before the commit ran. Nothing was written.
  • COMMIT_INTERRUPTED — the attempt stopped partway through and was deliberately not retried, because it may already have written a template. Look in the library for a template made from this draft before you commit it again, or you may end up with two.
  • INTERNAL_ERROR — the commit could not be queued or run, including when the acting member can no longer be resolved.
note

Nothing reclaims a commit that never finishes. A draft whose commit stays running cannot be committed again, and a repeated POST keeps answering running. Use queuedDateTime to decide when to stop waiting. The draft's document is still readable, so you can start a new draft from it.

Create a Draft​

Starts a draft. Send no body for an empty draft, document to start from a document you already have, or fromTemplateKey to seed the draft from an existing template. The response carries the document and its validation — an empty draft is invalid, because a template needs a name, and that is expected.

Seeding does two things. It exports the template into the draft, in the same form GET /v3/templates/{key}/document returns, and it records the template as basedOnTemplateKey, so that committing the draft writes a new version of that template instead of a new template. Seeding is the safe way to change an existing template, because the export carries the permissions, tags and task and field ids that a hand-written document would drop. To copy a template without the lineage — a draft that commits as a new template — export it yourself and send the result as document.

POST /v3/drafts

Parameters​

This endpoint takes no parameters.

Request Body​

{
"fromTemplateKey": "9a1d5e7c-2b3f-4c8a-8e6d-1f0b7c4a2d93",
"fromVersion": 2
}

The body is optional and every field in it is optional.

FieldTypeRequiredDescription
documentobjectNoA template document to start from. Stored without being validated; the response says what is wrong with it. Cannot be sent with fromTemplateKey.
fromTemplateKeystring (GUID)NoA template to seed the draft from. Also makes the eventual commit a new version of that template.
fromVersionintegerNoWhich version of fromTemplateKey to seed from. Omit for the latest. Only allowed with fromTemplateKey. A draft seeded from an older version still commits as the next version, holding that older version's content.

Example​

POST https://api.checkflow.io/v3/drafts
X-API-KEY: your-api-key-here
HTTP/1.1 201 Created
ETag: "1"
{
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14",
"revision": 1,
"document": {
"tags": [],
"permissions": [],
"parameters": [],
"tasks": [],
"conditions": [],
"notifications": []
},
"validation": {
"valid": false,
"errorCount": 1,
"warningCount": 0,
"violations": [
{
"path": "$.name",
"code": "property_required",
"message": "This needs a name, and none was given.",
"severity": "error"
}
]
},
"createdDateTime": "2026-09-21T10:15:32.41Z",
"updatedDateTime": "2026-09-21T10:15:32.41Z",
"expiresDateTime": "2026-10-21T10:15:32.41Z"
}

Responses​

StatusCodeWhen
201—The draft was created. Returns a Draft object with an ETag header.
400VALIDATION_ERRORThe body could not be read; both document and fromTemplateKey were sent (field fromTemplateKey); fromVersion was sent without fromTemplateKey (field fromVersion); or fromTemplateKey is not a GUID (field fromTemplateKey).
403FORBIDDENfromTemplateKey names a template in a Library folder where the Member the key acts as has Deny.
404TEMPLATE_NOT_FOUNDfromTemplateKey names no template in this workspace, or the template has no such fromVersion.

List Drafts​

Returns the workspace's drafts, most recently changed first, without their documents or validation. Read a single draft to get those. Expired drafts are not included. Committed drafts are, for the seven days they are kept, and carry committedTemplateKey, committedVersion and committedDateTime.

Check this list before starting a draft for something you may already have half-written.

GET /v3/drafts

Parameters​

This endpoint takes no parameters. It is not paged: every live draft is returned in one response, hasMore is always false and nextCursor is absent.

Example​

GET https://api.checkflow.io/v3/drafts
X-API-KEY: your-api-key-here
HTTP/1.1 200 OK
{
"items": [
{
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14",
"name": "Invoice Review",
"revision": 3,
"createdDateTime": "2026-09-21T10:15:32.41Z",
"updatedDateTime": "2026-09-21T10:17:21.06Z",
"expiresDateTime": "2026-10-21T10:17:21.06Z"
},
{
"key": "6c8e1a2f-4d7b-4b9e-a3c5-8f2d0e6b1a47",
"name": "Employee Onboarding",
"revision": 5,
"basedOnTemplateKey": "2e7b4d91-5c3a-4f8e-9b16-0d4a8c2f7e35",
"committedTemplateKey": "2e7b4d91-5c3a-4f8e-9b16-0d4a8c2f7e35",
"committedVersion": 4,
"committedDateTime": "2026-09-18T14:02:47.3Z",
"createdDateTime": "2026-09-17T09:40:12.88Z",
"updatedDateTime": "2026-09-18T13:55:09.61Z",
"expiresDateTime": "2026-09-25T14:02:48.12Z"
}
],
"hasMore": false,
"total": 2
}

Responses​

StatusCodeWhen
200—Returns the drafts as Draft objects without document and validation.

Get a Draft​

Returns one draft with its document and the current validation of that document. Reading does not extend the draft's expiry.

GET /v3/drafts/{key}

Parameters​

NameInTypeRequiredDescription
keypathstring (GUID)YesThe draft key.

Example​

GET https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14
X-API-KEY: your-api-key-here
HTTP/1.1 200 OK
ETag: "2"

The body is a Draft object, as in the example there.

Responses​

StatusCodeWhen
200—Returns a Draft object with an ETag header.
400VALIDATION_ERRORkey is not a GUID.
404DRAFT_NOT_FOUNDNo such draft in this workspace, or it has expired.

Replace a Draft's Document​

Replaces the draft's document whole: what is stored afterwards is what you sent, and anything you leave out is removed. The document is not validated before it is stored — the response says what is wrong with it. Use this when you hold the entire document anyway; otherwise PATCH or operations change part of it without resending the rest.

PUT /v3/drafts/{key}

Parameters​

NameInTypeRequiredDescription
keypathstring (GUID)YesThe draft key.
If-MatchheaderstringNoThe revision this write is against. Without it the write is unconditional. See Revisions and Concurrency.

Request Body​

The body is a whole template document — not wrapped in a document property.

{
"name": "Invoice Review",
"description": "Checks a supplier invoice before it is paid.",
"tasks": [
{
"ref": "receive_invoice",
"name": "Receive the invoice",
"fields": [
{ "ref": "invoice_number", "type": "ShortText", "label": "Invoice number", "isRequired": true }
]
}
]
}

Example​

PUT https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14
X-API-KEY: your-api-key-here
Content-Type: application/json
If-Match: "1"

{
"name": "Invoice Review",
"description": "Checks a supplier invoice before it is paid.",
"tags": ["q3-audit"],
"tasks": [
{
"ref": "receive_invoice",
"name": "Receive the invoice",
"fields": [
{ "ref": "invoice_number", "type": "ShortText", "label": "Invoice number", "isRequired": true }
]
}
]
}
HTTP/1.1 200 OK
ETag: "2"

The body is the draft at its new revision, as in the Draft object example.

Responses​

StatusCodeWhen
200—The document was replaced. Returns the Draft object at its new revision, with an ETag header.
400VALIDATION_ERRORkey is not a GUID, the body could not be read as a template document, or If-Match is not a revision (field If-Match).
404DRAFT_NOT_FOUNDNo such draft in this workspace, or it has expired.
409CONFLICTIf-Match is stale — the message names the current revision — or the draft has been committed and can no longer be edited.

Patch a Draft's Document​

Changes part of the draft's document without resending the rest. The body is a JSON Merge Patch (RFC 7386): an object shaped like the document, naming only what changes. Use it for the name, description, settings, tags and permissions.

A merge patch has three rules:

  • An object recurses. {"settings": {"displayComments": false}} changes one setting and leaves the others as they were.
  • null removes the member it names. {"description": null} clears the description. Leaving a member out means "do not touch it".
  • Everything else replaces, arrays included. {"tags": ["urgent"]} leaves the draft with exactly one tag. {"tasks": [...]} replaces every task. To change one entry in a list, use operations.

A PATCH is always a conditional write — see Revisions and Concurrency. A patch with no members, {}, changes nothing but still counts as a write: it moves the revision on and extends the draft's expiry.

PATCH /v3/drafts/{key}

Parameters​

NameInTypeRequiredDescription
keypathstring (GUID)YesThe draft key.
If-MatchheaderstringNoThe revision this patch is against. Without it, the patch is checked against the revision the request read.

Request Body​

Send Content-Type: application/merge-patch+json; application/json is accepted too. The body must be a JSON object.

{
"name": "Invoice Review",
"description": null,
"settings": { "displayComments": false }
}

Any member of the template document may appear. A settings object patched onto a draft that has no settings yet is filled in with the defaults a new template gets for everything you did not name.

Example​

PATCH https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14
X-API-KEY: your-api-key-here
Content-Type: application/merge-patch+json
If-Match: "1"

{
"name": "Invoice Review",
"description": "Checks a supplier invoice before it is paid.",
"tags": ["q3-audit"]
}
HTTP/1.1 200 OK
ETag: "2"
{
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14",
"name": "Invoice Review",
"revision": 2,
"document": {
"name": "Invoice Review",
"description": "Checks a supplier invoice before it is paid.",
"tags": ["q3-audit"],
"permissions": [],
"parameters": [],
"tasks": [],
"conditions": [],
"notifications": []
},
"validation": {
"valid": true,
"errorCount": 0,
"warningCount": 0,
"violations": []
},
"createdDateTime": "2026-09-21T10:15:32.41Z",
"updatedDateTime": "2026-09-21T10:16:08.72Z",
"expiresDateTime": "2026-10-21T10:16:08.72Z"
}

Responses​

StatusCodeWhen
200—The patch was applied. Returns the Draft object at its new revision, with the whole document and an ETag header.
400VALIDATION_ERRORkey is not a GUID; the body is not JSON or not a JSON object; the patch left the document unreadable, for example a number where the name goes (field names where, such as $.name); or If-Match is not a revision. Nothing is written.
404DRAFT_NOT_FOUNDNo such draft in this workspace, or it has expired.
409CONFLICTThe draft changed since the revision checked — the message names the current revision — or the draft has been committed and can no longer be edited.

Notes​

  • A member the document does not recognise, such as a misspelled property, is accepted and dropped. The response carries the whole document so you can see what the patch actually did.
  • PATCH does not reach inside the document's lists. For tasks, fields, parameters, conditions and notifications use operations.

Apply Operations to a Draft​

Changes the draft's document one node at a time: a single task, a field on it, a parameter, a condition or a notification. This reaches what PATCH cannot, because a merge patch replaces arrays whole. The operations are described in the Operations Reference.

The operations are applied in the order sent, and each one sees the document as the one before it left it — so one request can add a task and then add fields to it by the ref it just gave. The list is all or nothing: if any operation is refused, nothing is written and the draft is unchanged. A list that succeeds moves the revision by one, however many operations it contains.

Removals do not cascade. Removing a task removes its fields, but conditions, notifications and due dates that pointed at it are left pointing at nothing, and come back as violations in validation on the same response. Fix or remove them yourself.

POST /v3/drafts/{key}/operations

Parameters​

NameInTypeRequiredDescription
keypathstring (GUID)YesThe draft key.
If-MatchheaderstringNoThe revision these operations are against. Without it, they are checked against the revision the request read.

Request Body​

{
"operations": [
{ "op": "add_task", "ref": "approve_payment", "value": { "name": "Approve payment" } },
{ "op": "add_field", "task": "approve_payment", "value": { "type": "ShortText", "label": "Approved by" } }
]
}
FieldTypeRequiredDescription
operationsarrayYesThe operations, in the order to apply them. At least one — an empty list is refused.
operations[].opstringYesWhich operation. One of the 19 names in the Operations Reference.
operations[].refstringDependsThe node the operation is about. Required by update_*, remove_* and move_*; optional on add_*; not taken by set_*.
operations[].taskstringDependsThe ref of a task. Required by add_field; optional on move_field; not taken by anything else.
operations[].afterstringNoPlace the node immediately after this sibling's ref. Taken by add_task, add_field, move_task and move_field only.
operations[].beforestringNoPlace the node immediately before this sibling's ref. Same operations as after. Cannot be sent with after.
operations[].valueanyDependsThe new node for add_*, a merge patch for update_*, the member's new value for set_*. Not taken by remove_* and move_*.

A member an operation does not take is refused rather than ignored — a value on remove_task or a before on add_parameter is a 400.

Example​

POST https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14/operations
X-API-KEY: your-api-key-here
Content-Type: application/json
If-Match: "2"

{
"operations": [
{ "op": "add_task", "ref": "receive_invoice", "value": { "name": "Receive the invoice" } },
{
"op": "add_field",
"ref": "invoice_number",
"task": "receive_invoice",
"value": { "type": "ShortText", "label": "Invoice number", "isRequired": true }
},
{ "op": "add_task", "ref": "approve_payment", "value": { "name": "Approve payment" } },
{
"op": "add_field",
"task": "approve_payment",
"value": {
"type": "DropDown",
"label": "Decision",
"isRequired": true,
"items": [
{ "ref": "decision_approve", "text": "Approve" },
{ "ref": "decision_reject", "text": "Reject" }
]
}
}
]
}
HTTP/1.1 200 OK
ETag: "3"
{
"key": "3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14",
"name": "Invoice Review",
"revision": 3,
"document": {
"name": "Invoice Review",
"description": "Checks a supplier invoice before it is paid.",
"tags": ["q3-audit"],
"permissions": [],
"parameters": [],
"tasks": [
{
"ref": "receive_invoice",
"name": "Receive the invoice",
"isHeading": false,
"halt": "none",
"hiddenByDefault": false,
"assignTo": [],
"assignFrom": [],
"assignedExclusively": false,
"tags": [],
"fields": [
{
"ref": "invoice_number",
"type": "ShortText",
"hiddenByDefault": false,
"label": "Invoice number",
"isRequired": true
}
]
},
{
"ref": "approve_payment",
"name": "Approve payment",
"isHeading": false,
"halt": "none",
"hiddenByDefault": false,
"assignTo": [],
"assignFrom": [],
"assignedExclusively": false,
"tags": [],
"fields": [
{
"ref": "decision",
"type": "DropDown",
"hiddenByDefault": false,
"label": "Decision",
"isRequired": true,
"items": [
{ "ref": "decision_approve", "text": "Approve" },
{ "ref": "decision_reject", "text": "Reject" }
]
}
]
}
],
"conditions": [],
"notifications": []
},
"validation": {
"valid": true,
"errorCount": 0,
"warningCount": 0,
"violations": []
},
"applied": [
{ "op": "add_task", "ref": "receive_invoice" },
{ "op": "add_field", "ref": "invoice_number" },
{ "op": "add_task", "ref": "approve_payment" },
{ "op": "add_field", "ref": "decision" }
],
"createdDateTime": "2026-09-21T10:15:32.41Z",
"updatedDateTime": "2026-09-21T10:17:21.06Z",
"expiresDateTime": "2026-10-21T10:17:21.06Z"
}

The last add_field gave no ref, so one was made from the field's label — decision — and reported in applied.

Responses​

StatusCodeWhen
200—Every operation was applied. Returns the Draft object at its new revision, with applied and an ETag header.
400VALIDATION_ERRORAn operation did not make sense — see below. field names the operation and member, for example $.operations[2].ref. Nothing is written. Also returned for a key that is not a GUID, an unreadable body or an If-Match that is not a revision.
404DRAFT_NOT_FOUNDNo such draft in this workspace, or it has expired.
409CONFLICTThe draft changed since the revision checked — the message names the current revision — or the draft has been committed and can no longer be edited.

An operation is refused with 400 when:

  • the list is empty (field $.operations);
  • op is missing or unknown — the message suggests the nearest operation name;
  • a ref, task, after or before names nothing — the message suggests the nearest ref that exists;
  • an add names a ref already used anywhere in the document, or a ref that is not a valid ref;
  • the operation's ref and the ref inside its value disagree;
  • a member is sent that the operation does not take, or a required one is missing;
  • after and before are both sent, or a move says neither;
  • a node is moved relative to itself;
  • an update removes a node's ref ("ref": null) or renames it to one already in use;
  • value is not the right kind of JSON, or cannot be read as the node — field points into the value, for example $.operations[2].value.halt.

Operations Reference​

There are 19 operations: five verbs over five kinds of node, plus two setters.

VerbWhat it does
addCreates a node. value is the node, in the same shape the template document uses — a task read out of one document can be sent straight into another.
updateMerges value into an existing node as a JSON merge patch, with the same three rules as PATCH: objects recurse, null removes, everything else — arrays included — replaces.
removeDeletes a node.
moveReorders a node among its siblings. Only tasks and fields have an order.
setReplaces a whole member of the document.
opreftaskafter / beforevalue
add_taskOptional—OptionalTask
update_taskRequired——Merge patch
remove_taskRequired———
move_taskRequired—One required—
add_fieldOptionalRequiredOptionalField
update_fieldRequired——Merge patch
remove_fieldRequired———
move_fieldRequiredOptionalOptional—
add_parameterOptional——Parameter
update_parameterRequired——Merge patch
remove_parameterRequired———
add_conditionOptional——Condition
update_conditionRequired——Merge patch
remove_conditionRequired———
add_notificationOptional——Notification
update_notificationRequired——Merge patch
remove_notificationRequired———
set_settings———Settings object
set_permissions———Permissions array

Refs and Positions​

Every node is addressed by its ref, never by an index. A ref means the same node before and after every operation in a list, even when earlier operations removed things.

  • A ref is unique across the whole document, not per kind: a task and a field cannot share one. A valid ref is 1 to 64 characters of lower-case letters, digits, underscores and hyphens, starting with a letter or a digit.
  • On an add, give the ref either as the operation's ref or inside value — not both with different values. If you give none, one is made from the node's name the same way export makes them, numbered if that word is taken: a task named Order the kit becomes order_the_kit, and a second one order_the_kit_2. A field is named from its label or text, or after its type (such as text or separator) when it has neither. Conditions and notifications have no name, so they become condition, condition_2 and notification, notification_2. Read applied for refs you did not choose.
  • An add_task whose value includes fields gets refs made for any of those fields that have none.
  • after and before name a sibling: a task for task operations, a field on the destination task for field operations. There is no index and no "first" keyword — to put a node at the top, send before the node that is currently first. With neither, an add appends to the end. A move must say where; to move to the end, send after the last node. A move's position is worked out after the node has left its old place.
  • Parameters, conditions and notifications have no order in CheckFlow, so their add operations always append and there is no move_parameter, move_condition or move_notification.
  • An update can rename a node by including ref in the patch. applied reports the new ref, which is the one later operations must use. A ref cannot be removed.

Tasks​

add_task​

Adds a task, at the end or next to a named task. The fields in value.fields come with it.

{ "op": "add_task", "ref": "check_po", "before": "approve_payment", "value": { "name": "Match the purchase order" } }

update_task​

Merges a patch into a task. Members you leave out are unchanged; arrays you send — fields, tags, assignTo — replace the task's whole list.

{ "op": "update_task", "ref": "approve_payment", "value": { "name": "Approve the payment", "halt": "task" } }

remove_task​

Removes a task and the fields on it. Conditions, notifications and due dates that referred to it become violations.

{ "op": "remove_task", "ref": "check_po" }

move_task​

Moves a task next to another task. after or before is required.

{ "op": "move_task", "ref": "approve_payment", "after": "receive_invoice" }

Fields​

Fields are a task's input and content controls — see Control Types. Their properties depend on type and are listed in Template Documents.

add_field​

Adds a field to the task named in task, at the end or next to a named field on that task.

{
"op": "add_field",
"ref": "amount",
"task": "receive_invoice",
"after": "invoice_number",
"value": { "type": "ShortText", "label": "Amount", "isRequired": true }
}

update_field​

Merges a patch into a field. The field is found by its ref alone — do not send task.

{ "op": "update_field", "ref": "invoice_number", "value": { "label": "Supplier invoice number" } }

remove_field​

Removes a field. Conditions that watched or showed it become violations.

{ "op": "remove_field", "ref": "amount" }

move_field​

Moves a field within its task, or onto another task. Send task to move it to a different task; after or before to position it. At least one of the three is required. With task and no position, the field goes to the end of that task.

{ "op": "move_field", "ref": "invoice_number", "task": "approve_payment", "before": "decision" }

Parameters​

Template parameters are appended in the order you add them.

add_parameter​

{ "op": "add_parameter", "ref": "supplier_name", "value": { "name": "Supplier name", "type": "string" } }

update_parameter​

{ "op": "update_parameter", "ref": "supplier_name", "value": { "defaultValue": "Unknown supplier" } }

remove_parameter​

{ "op": "remove_parameter", "ref": "supplier_name" }

Conditions​

Conditional logic rules. Prefer update_condition to removing and re-adding a rule: a rule that has been saved keeps its id through an update, and a re-added rule is a new one.

add_condition​

{
"op": "add_condition",
"ref": "show_rejection",
"value": {
"when": { "fieldRef": "decision", "operator": "is", "value": "decision_reject" },
"then": { "effect": "show", "tasks": ["record_rejection"] }
}
}

update_condition​

{ "op": "update_condition", "ref": "show_rejection", "value": { "then": { "effect": "hide" } } }

remove_condition​

{ "op": "remove_condition", "ref": "show_rejection" }

Notifications​

Custom notifications. Prefer update_notification to removing and re-adding: a notification keeps its id through an update, and checklists upgraded to the new version match their due-date reminders to the template's by that id.

add_notification​

{
"op": "add_notification",
"ref": "payment_approved",
"value": {
"event": "completed",
"on": { "tasks": ["approve_payment"] },
"notify": [{ "name": "Finance Team" }]
}
}

update_notification​

{
"op": "update_notification",
"ref": "payment_approved",
"value": { "notify": [{ "name": "Finance Team" }, { "email": "priya.patel@acme.example" }] }
}

remove_notification​

{ "op": "remove_notification", "ref": "payment_approved" }

Settings and Permissions​

set_settings​

Replaces the document's settings whole. Any setting you leave out goes back to the default a new template gets — it does not keep its current value. To change one setting and keep the rest, use PATCH with {"settings": {...}} instead.

{ "op": "set_settings", "value": { "displayComments": false, "displayTaskTags": true } }

set_permissions​

Replaces the document's template permissions whole. value is an array; an empty array clears them.

{
"op": "set_permissions",
"value": [
{ "type": "AllUsers", "permission": "ViewOnly" },
{ "type": "Group", "name": "Finance Team", "permission": "RunAndView" }
]
}

There are no operations for the document's name, description or tags. Change those with PATCH.

Validate a Draft​

Returns what is wrong with the draft's document as it stands now, without writing anything. Every other draft response already includes this block; call this on its own before committing a draft that has been sitting for a while, because validation reflects the workspace now. It does not extend the draft's expiry.

For a seeded draft, the document is validated as a new version of its seed template, which is how the commit will treat it. If the seed has since been deleted, validation carries on without it; the commit is where that is refused.

POST /v3/drafts/{key}/validate

Parameters​

NameInTypeRequiredDescription
keypathstring (GUID)YesThe draft key.

Request Body​

This endpoint takes no request body.

Example​

POST https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14/validate
X-API-KEY: your-api-key-here
HTTP/1.1 200 OK
{
"valid": true,
"errorCount": 0,
"warningCount": 0,
"violations": []
}

Responses​

StatusCodeWhen
200—Returns a Validation object. A document with errors is still a 200 — read valid.
400VALIDATION_ERRORkey is not a GUID.
404DRAFT_NOT_FOUNDNo such draft in this workspace, or it has expired.

Commit a Draft​

Turns the draft into a template. What it makes is decided by the draft, not by this request:

Draft startedCommit makesisNewTemplate
Empty, or from documentA new template, at version 1true
From fromTemplateKeyThe next version of that templatefalse

Everything that can be refused is refused in this request, before anything is queued: a missing draft, a stale If-Match, a seed template that has been deleted or archived, a key acting as a Member who may not save the template (see Who Can See a Draft), and — above all — a document with errors, which is answered 422 with every violation. The draft is left exactly as it was, so you can fix what the refusal names and commit again.

Writing a large template can take longer than a request may last, so the writing itself is queued. The response is 202 Accepted with status queued, a Location header and Retry-After: 2. Poll GET /v3/drafts/{key}/commit — the same path — until status is committed or failed.

The route is safe to retry. If a commit of this draft is already under way, it answers 202 with status running. If the draft has already been committed, it answers 200 with the full receipt — templateKey, version, refMap — and isReplay true, rather than making a second template. This is why a committed draft is kept for seven days.

POST /v3/drafts/{key}/commit

Parameters​

NameInTypeRequiredDescription
keypathstring (GUID)YesThe draft key.
If-MatchheaderstringNoThe revision to commit. The commit is refused if the draft has changed since. It is checked when the commit is accepted and again when the template is written.

Request Body​

This endpoint takes no request body.

Example​

POST https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14/commit
X-API-KEY: your-api-key-here
If-Match: "3"
HTTP/1.1 202 Accepted
Location: /api/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14/commit
Retry-After: 2
{
"status": "queued",
"isNewTemplate": true,
"isReplay": false,
"queuedDateTime": "2026-09-21T10:18:04.53Z"
}

A document with errors is refused before anything is queued:

HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "TEMPLATE_INVALID",
"message": "The template document has 1 problem.",
"requestId": "7c4e2a91-3b8d-4f60-a5e1-9d2c6b0f8e37",
"violations": [
{
"path": "$.name",
"code": "property_required",
"message": "This needs a name, and none was given.",
"severity": "error"
}
]
}
}

Responses​

StatusCodeWhen
202—The commit was accepted. Returns a Commit object with status queued (this request started it) or running (one was already under way), plus Location and Retry-After headers.
200—The draft had already been committed. Returns the Commit object receipt with status committed and isReplay true. Nothing was done.
400VALIDATION_ERRORkey is not a GUID, or If-Match is not a revision.
403FORBIDDENThe key acts as a Member without Template.Creator, or — for a draft seeded from a template — whose permission on that template's Library folder is only View or Deny. Nothing was queued.
404DRAFT_NOT_FOUNDNo such draft in this workspace, or it has expired.
409CONFLICTThe draft has changed since the If-Match revision, or it was seeded from a template that has since been deleted or archived. For a deleted seed, start a new draft from this one's document — it commits as a new template. For an archived seed, restore the template first.
422TEMPLATE_INVALIDThe document has errors. error.violations lists every one. Nothing was queued and the draft is unchanged.

Notes​

  • Warnings do not stop a commit; only errors do.
  • A commit that writes a new version of a template rewrites that template's permissions and tags, which are stored once per template rather than per version. A seeded draft carries them from the export; if you edited them away, the new version removes them from every version. See Template Versioning.
  • When the template is written, the rest of the team is notified that it was created or modified, as for POST /v3/templates.
  • A commit does not change the draft's revision and the response carries no ETag. Once committed, the draft cannot be edited.
warning

Do not edit a draft while its commit is queued or running. Edits are not refused during that time. If you committed with If-Match, the commit then fails with CONFLICT — after the template has already been written, as the error message says. If you committed without If-Match, the draft is marked committed even though your later edit is not in the template.

Get Commit Status​

Reports how a draft's commit is going, or how it went. This is the URL to poll after a 202 from Commit a Draft. It returns the same Commit object in every state, and it is always 200 while the draft exists — a failed commit is a status, not a failed request.

Wait Retry-After seconds (2) between polls; most commits finish within that. This route is charged to the cheap rate limit budget and does not extend the draft's expiry.

GET /v3/drafts/{key}/commit

Parameters​

NameInTypeRequiredDescription
keypathstring (GUID)YesThe draft key.

Example​

GET https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14/commit
X-API-KEY: your-api-key-here
HTTP/1.1 200 OK
{
"status": "running",
"isNewTemplate": true,
"isReplay": true,
"queuedDateTime": "2026-09-21T10:18:04.53Z"
}

A few seconds later the same request returns the finished commit, as in the Commit object example. A failed commit looks like this:

{
"status": "failed",
"isNewTemplate": true,
"isReplay": true,
"queuedDateTime": "2026-09-21T10:18:04.53Z",
"error": {
"code": "COMMIT_INTERRUPTED",
"message": "The commit of draft '3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14' was interrupted and has not been retried, because the attempt that was interrupted may already have written a template. Check the library for a template made from this draft before committing it again."
}
}

Responses​

StatusCodeWhen
200—Returns a Commit object in whichever of the five states the commit is in.
400VALIDATION_ERRORkey is not a GUID.
404DRAFT_NOT_FOUNDNo such draft in this workspace, or it has expired. A committed draft expires seven days after its commit, so this is also the answer long afterwards.

Delete a Draft​

Deletes a draft. Committed drafts can be deleted too: you give up the seven-day receipt, not the template it made. Drafts expire on their own, so you do not need to delete drafts you have finished with.

danger

Deleting a draft is permanent. Unless the draft has been committed, its document exists nowhere else.

DELETE /v3/drafts/{key}

Parameters​

NameInTypeRequiredDescription
keypathstring (GUID)YesThe draft key.

Example​

DELETE https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14
X-API-KEY: your-api-key-here

Responds 204 No Content with no body.

Responses​

StatusCodeWhen
204—The draft was deleted.
400VALIDATION_ERRORkey is not a GUID.
404DRAFT_NOT_FOUNDNo such draft in this workspace, or it has expired — including a draft you have already deleted. A second delete is a 404, not a second 204.

Walkthrough: Build and Commit a Template​

This builds a small Invoice Review template from nothing. Each step sends the revision it last read as If-Match.

  1. Create an empty draft. POST /v3/drafts with no body. The response is 201 with revision 1 and one violation — the document has no name. Keep the key.

    POST https://api.checkflow.io/v3/drafts
    X-API-KEY: your-api-key-here
  2. Name and describe it. Patch the members outside the lists. The response has revision 2 and no violations.

    PATCH https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14
    X-API-KEY: your-api-key-here
    Content-Type: application/merge-patch+json
    If-Match: "1"

    {
    "name": "Invoice Review",
    "description": "Checks a supplier invoice before it is paid.",
    "tags": ["q3-audit"]
    }
  3. Add the tasks and fields. One operations request adds both tasks and their fields; the later operations refer to tasks added by earlier ones. The response has revision 3, and applied reports the ref invented for the field that was sent without one. The full request and response are the example under Apply Operations to a Draft.

  4. Check it. POST /v3/drafts/{key}/validate returns valid true. If it lists errors, fix them with more operations or patches and check again.

    POST https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14/validate
    X-API-KEY: your-api-key-here
  5. Commit it. The response is 202 with status queued and Retry-After: 2.

    POST https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14/commit
    X-API-KEY: your-api-key-here
    If-Match: "3"
  6. Poll until it finishes. Wait two seconds, then ask. Repeat while status is queued or running.

    GET https://api.checkflow.io/v3/drafts/3f2b8c1e-7a4d-4e5b-9c61-2d8f0a7b6e14/commit
    X-API-KEY: your-api-key-here

    When status is committed, the response carries the new template's key, its version and the refMap:

    {
    "status": "committed",
    "templateKey": "9a1d5e7c-2b3f-4c8a-8e6d-1f0b7c4a2d93",
    "version": 1,
    "isNewTemplate": true,
    "isReplay": true,
    "queuedDateTime": "2026-09-21T10:18:04.53Z",
    "committedDateTime": "2026-09-21T10:18:06.17Z",
    "refMap": {
    "receive_invoice": "5b7e2c91-8a4f-4d63-b0e2-7c1f9a3d6e58",
    "invoice_number": "d2a64f18-3c7b-4e95-8f1a-6b0c2e9d7a34",
    "approve_payment": "7f3c1b8e-6d2a-4a57-9e0b-4c8f2d1a6b93",
    "decision": "a8e5d2c7-1f4b-4c69-b3d0-9e7a5f2c8b16"
    }
    }

    If status is failed, read error, fix the draft and commit again.

  7. Use the template. Read it at GET /v3/templates/9a1d5e7c-2b3f-4c8a-8e6d-1f0b7c4a2d93 — see Templates — and start checklists from it.

To change the template later, start a new draft with {"fromTemplateKey": "9a1d5e7c-2b3f-4c8a-8e6d-1f0b7c4a2d93"}, edit it the same way and commit it. The commit writes version 2.

MCP Tools​

The CheckFlow MCP server offers the same routes as tools, listed in the Endpoints table. The differences:

  • The draft key is the draftKey argument, and If-Match is the ifRevision argument (a number) on replace_template_draft, update_template_draft, apply_draft_operations and commit_template.
  • update_template_draft takes the merge patch as a free-form patch object, because a typed argument cannot tell a member left out from a member sent as null.
  • create_template_draft, apply_draft_operations and delete_template_draft take an idempotencyKey argument.
  • commit_template answers queued or running like the REST route; call get_commit_status until it says committed or failed.
  • Template Documents — the document format every draft holds, including every field type and violation code.
  • Templates — creating and versioning templates in one request, and reading the template a commit made.
  • Template Versioning — what a new version changes for checklists already running.
  • Errors — the error body and every code, including DRAFT_NOT_FOUND and COMMIT_INTERRUPTED.
  • Idempotency — making retries of operations and other writes safe.