Skip to content
Dashboard

Task templates

A task template is a reusable checklist. It has a header (name, owning department, default priority), requirement groups (sections such as “Bathrooms” or “Final photo”), and checklist lines inside those groups. Applying a template to a task copies its requirements onto that task, repeated for each matching element of the task’s property. This guide also covers a task’s own requirements: listing them, adding one-off ones and rewording them. Every field is listed on the operation’s page in the API Reference.

OperationRequestPermission
listTemplatesGET /task-templatesview_task_templates
getTemplateGET /task-templates/{id}view_task_templates
createTemplatePOST /task-templatesmanage_task_templates
updateTemplatePATCH /task-templates/{id}manage_task_templates
addTemplateGroupPOST /task-template-groupsmanage_task_templates
updateTemplateGroupPATCH /task-template-groups/{id}manage_task_templates
addTemplateChecklistItemPOST /task-template-checklist-itemsmanage_task_templates
updateTemplateChecklistItemPATCH /task-template-checklist-items/{id}manage_task_templates
applyTemplateToTaskPOST /task-template-applicationsmodify_tasks
listTaskRequirementsGET /task-requirements?taskId={taskId}view_tasks
addTaskRequirementPOST /task-requirementsmodify_tasks
updateTaskRequirementPATCH /task-requirements/{id}modify_tasks

Paths are relative to your region’s base URL, for example https://api-us.suiteop.com/api/v1. See Authentication for permissions.

The API can’t delete or duplicate a template, a group, a checklist line or a task requirement. Those actions are only available in the app.

GET /task-templates is paginated (default limit 20, maximum 100). Rows are ordered oldest first.

Terminal window
curl "https://api-us.suiteop.com/api/v1/task-templates?nameContains=turnover&limit=50" \
-H "Authorization: Bearer sk_live_your_key_here"
  • nameContains is a case-insensitive substring, at most 100 characters. % and _ match literally, not as wildcards. It matches the stored name and the name’s translation in language.
  • departmentId keeps the templates owned by one department.
  • includeDeleted=true also returns templates deleted in the app. Their deletedAt is set.
  • language (for example es) chooses the language of name. When there is no translation in that language, you get the stored name. The default is English.

Each row has the header fields plus departmentName, departmentColor, departmentCategory, groupCount and itemCount. The rows don’t include groups or checklist lines. groupCount leaves out archived groups, but itemCount counts every checklist line on the template, including lines in archived groups.

GET /task-templates/{id} returns the header and its groups, each with its checklistItems. Groups are sorted by rank, then by creation time. Lines within a group are sorted the same way. Groups removed in the app are archived and not returned.

Text comes back resolved: nameText, descriptionText, each group’s titleText and photoAiVerificationPromptText, and each line’s descriptionText. Each is in English, or in the earliest language it was written in if there’s no English version. This operation has no language parameter.

{
"data": {
"id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"name": "Standard turnover clean",
"nameText": "Standard turnover clean",
"description": null,
"descriptionText": null,
"priority": "medium",
"isAdminTemplate": false,
"departmentId": "0b6f2c1e-4d3a-4b2c-9e8f-7a6b5c4d3e2f",
"requiredSkillId": null,
"createdById": "5c2b7a10-3e4f-4a5b-8c6d-9e0f1a2b3c4d",
"deletedAt": null,
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:05:00.000Z",
"groups": [
{
"id": "a7b8c9d0-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"titleText": "Bathrooms",
"rank": 0,
"isArchived": false,
"reference": null,
"userCreated": false,
"photoRequired": false,
"photoRequirement": "disabled",
"targetCategoryId": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
"targetLevel": "area",
"parentCategoryId": null,
"type": "checklist",
"createdById": "5c2b7a10-3e4f-4a5b-8c6d-9e0f1a2b3c4d",
"templateId": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"optionSetId": null,
"createdAt": "2026-09-29T10:01:00.000Z",
"updatedAt": "2026-09-29T10:01:00.000Z",
"photoAiVerificationPromptText": null,
"checklistItems": [
{
"id": "f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f",
"rank": 0,
"descriptionText": "Scrub the shower",
"userCreated": false,
"createdById": "5c2b7a10-3e4f-4a5b-8c6d-9e0f1a2b3c4d",
"templateId": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"groupId": "a7b8c9d0-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"optionGroupId": null,
"createdAt": "2026-09-29T10:02:00.000Z",
"updatedAt": "2026-09-29T10:02:00.000Z"
}
]
}
],
"translationCoverage": { "en": 3 },
"translationFieldCount": 3
},
"meta": { "requestId": "3f1c9a52-…" }
}

A template’s estimated time and cost belong to its rate rules, not to the template itself, so this response doesn’t show them.

POST /task-templates creates the template header only. It starts with no requirements. You then add groups with addTemplateGroup and add lines to them with addTemplateChecklistItem.

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/task-templates \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 0f8e3a52-6c1d-4b7e-9a2f-5d4c3b2a1e0f" \
-H "Content-Type: application/json" \
-d '{
"name": "Standard turnover clean",
"departmentId": "0b6f2c1e-4d3a-4b2c-9e8f-7a6b5c4d3e2f",
"priority": "high",
"description": "Full clean between stays"
}'

The response is 201 with the stored row:

{
"data": {
"id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"name": "Standard turnover clean",
"description": "Full clean between stays",
"priority": "high",
"isAdminTemplate": false,
"departmentId": "0b6f2c1e-4d3a-4b2c-9e8f-7a6b5c4d3e2f",
"requiredSkillId": null,
"createdById": "5c2b7a10-3e4f-4a5b-8c6d-9e0f1a2b3c4d",
"deletedAt": null,
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:00:00.000Z"
},
"meta": { "requestId": "3f1c9a52-…" }
}
  • name and departmentId are required. The name is trimmed and must be 1 to 255 characters long, counted as Unicode code points, so a blank name is rejected and most emoji count as one.
  • departmentId must be an active department in your organization (use listDepartments). An archived department or one from another organization returns 404.
  • priority is the default priority for the template’s tasks: watch, low, medium, high or urgent. It defaults to medium.
  • requiredSkillId is a skill a member needs before a task from this template can be assigned to them. No operation lists skills, but getMember returns one member’s skills with their IDs. An ID from another organization returns 404.
  • isAdminTemplate: true hides the template from the pickers ordinary staff see. It defaults to false.
  • language is the language name and description are written in. It defaults to en.

PATCH /task-templates/{id} takes a data object. Fields you leave out keep their current value.

Terminal window
curl -X PATCH "https://api-us.suiteop.com/api/v1/task-templates/c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f" \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"data": {"priority": "urgent", "requiredSkillId": null}}'
  • You can change name, description, departmentId, priority, requiredSkillId and isAdminTemplate.
  • description: null removes the description, and requiredSkillId: null removes the skill requirement. departmentId can be changed to another department but can’t be cleared.
  • data must name at least one field. An empty data, or one with only language, returns 400 validation_error, and its details.issues entry says At least one field to change must be provided.
  • language sets which language a new name or description is written in (default en).
  • The response is the header row. Group and line text only comes back from getTemplate.

This operation edits the header only. Groups and lines have their own update operations.

A requirement group is a section of the template. Its type sets what the worker records: single_choice, yes_no, checklist, photo, count, text or star_rating. Checklist lines belong to a group. You need at least one group before you can add a line.

single_choice asks the worker to pick one answer from an answer list. Pass the list’s optionSetId; leave it out and the group uses the Item Condition Check list (Good / Worn / Damaged / Missing / N/A). On a template group bound to a scored list, the four switches failRequiresPhoto, failRequiresIssue, flagRequiresPhoto and flagRequiresIssue decide what a Fail or Flag answer demands before the task can complete; any you leave out are copied from the list’s defaults. A Result requires a photo or an issue, never both: turning one on turns its partner off, and if you send both on, the issue wins. addTaskRequirement doesn’t take them.

yes_no is a shortcut, not a stored type: it creates a single_choice group bound to one of SuiteOp’s Yes / No lists, so reading the group back returns type: "single_choice". Configure it with yesNo: { "problem": "none" | "yes" | "no", "includeNa": true | false } — problem is the answer that counts as a Fail. Without yesNo you get { "problem": "none", "includeNa": false }: an information-only question with no N/A. yesNo on any other type is rejected.

star_rating is a shortcut too: it creates a single_choice group bound to SuiteOp’s Star rating list, shown as a 1–5 scale. 1 and 2 stars are a Fail, 3 is a Flag, and 4 and 5 are a Pass, and each answer is worth its star count in points. None of the Fail or Flag switches is on by default. Reading the group back returns type: "single_choice". The old type name rating is still accepted and behaves the same, but use star_rating in new code.

The older dropdown and condition types are gone: use single_choice with the list you want.

POST /task-template-groups takes the templateId in the body.

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/task-template-groups \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 7b1e4c90-2d3f-4a5b-8c6d-1e2f3a4b5c6d" \
-H "Content-Type: application/json" \
-d '{
"templateId": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"title": "Bathrooms",
"type": "checklist",
"targetCategoryId": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
"targetLevel": "area",
"rank": 0
}'

The 201 response is the group row, whose id is what addTemplateChecklistItem takes. It doesn’t include the title text.

FieldNotes
titleRequired. Trimmed, and must not be blank. Written in language (default en)
targetCategoryIdAn element category from listElementCategories. The group repeats once for every element of that category on the task’s property. Leave it out for a general section that appears once
targetLevelarea or item. Only valid together with targetCategoryId
parentCategoryIdLimits the group to elements whose parent element is in this category. Rejected with targetLevel: "area". It must match the target category’s own parent when that category has one (400 otherwise)
typeLeave it out for an untyped group
rankPosition in the template, 0 or more, lower first. Defaults to 1. On the task, groups with the same rank are merged: their rows are ordered by element, then by line rank, so give each group its own rank
photoRequiredDefaults to false
photoRequirementdisabled (default), camera_only or camera_and_file
photoAiVerificationPromptWhat the AI verifier checks the photo against, for example the bed is made. Only checked on photo groups. A blank string means no prompt
allowVideoSet true to let a photo group accept a video as well as photos. Defaults to false. Only with type: "photo", and never together with photoAiVerificationPrompt, since the AI check judges photos only
optionSetIdThe answer list for a single_choice group, an ID from listAnswerLists. Leave it out for the Item Condition Check list. Ignored for every other type
failRequiresPhoto, failRequiresIssue, flagRequiresPhoto, flagRequiresIssuesingle_choice only: what a Fail or Flag answer demands. Left out, copied from the answer list. See Choice types
yesNoOnly with type: "yes_no". See Choice types

Several groups can share a target. That’s how one area gets both a checklist and a photo. An unknown category, parent category, option set or template returns 404.

POST /task-template-checklist-items adds one line per call. Make one call for each line.

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/task-template-checklist-items \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 2c9d8e7f-6a5b-4c3d-9e2f-1a0b9c8d7e6f" \
-H "Content-Type: application/json" \
-d '{
"groupId": "a7b8c9d0-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"description": "Scrub the shower",
"rank": 0
}'
  • groupId is a group ID from addTemplateGroup or getTemplate, not the template ID.
  • description is required. It’s trimmed and must not be blank.
  • rank defaults to 1, so send it whenever the order matters.
  • inventoryCatalogId links the line to an item type in your inventory catalog, so the number the worker enters is saved as a stock count. The group must be a count group, or the call returns 400. No operation lists the inventory catalog, so you’ll need to get the ID from the app.

The 201 response echoes id, groupId, templateId, rank and inventoryCatalogId, but not the text. getTemplate returns the text. It doesn’t return inventoryCatalogId, though, so check the link in the create or update response.

PATCH /task-template-groups/{id} and PATCH /task-template-checklist-items/{id} both take a data object with the fields to change. Any field you leave out keeps its current value. At least one field other than language is required.

Terminal window
curl -X PATCH "https://api-us.suiteop.com/api/v1/task-template-checklist-items/f0e1d2c3-b4a5-4968-8776-5a4b3c2d1e0f" \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"data": {"description": "Scrub and dry the shower", "rank": 2}}'

Group rules:

  • The targetLevel and parentCategoryId rules are checked against the fields you send, not against the stored group. A targetLevel sent without targetCategoryId is rejected even if the group already targets a category.
  • targetCategoryId: null turns the group into a general section and also clears its stored targetLevel and parentCategoryId.
  • allowVideo: true lets a photo group accept a video and clears its AI prompt. It is rejected on a non-photo group, a prompt sent to a video group is rejected unless the same call sends allowVideo: false, and retyping the group away from photo turns it off.
  • photoAiVerificationPrompt: null or "" removes the prompt. optionSetId: null on a single_choice group rebinds it to the Item Condition Check list. Sent without type, a non-null answer list on any other group is refused (400).
  • Changing the type of a group whose lines are linked to inventory to anything other than count returns 400 (validation.inventory_binding_requires_count_group).

Line rules:

  • groupId moves the line to another group on the same template. A group on a different template returns 400 (validation.checklist_item_move_same_template). An archived or unknown group returns 404.
  • inventoryCatalogId: null removes the inventory link. A new link needs a count group, including after a move.

createTask doesn’t accept a templateId over REST (see Tasks). Create the task, then apply the template with POST /task-template-applications:

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/task-template-applications \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" \
-H "Content-Type: application/json" \
-d '{
"taskId": "7d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a",
"templateId": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
}'
{
"data": {
"itemCount": 6,
"taskItemIds": ["2b3c4d5e-…", "…"]
},
"meta": { "requestId": "3f1c9a52-…" }
}

The task must have a property and must not be completed or cancelled. Otherwise the call returns 400 (validation.template_materialize_requires_property or validation.template_materialize_requires_open_task). A task outside your property access returns 404, as does a template from another organization.

What gets copied. Requirements are built from the template’s non-archived groups and the elements of the task’s property:

  • A general group (no targetCategoryId) adds one requirement per line, or a single requirement if the group has no lines.
  • A targeted group adds its lines once for each matching element, or one requirement per element if it has no lines. If the property has no matching element, the group adds nothing.
  • Order is the group’s rank, then the element’s position in the property’s element tree, then the line’s rank. The new requirements go after any the task already has.
  • Lines linked to inventory are matched to that property’s stock.

What isn’t copied. The task’s name, department, priority, skill and estimate stay as they are. The template only adds requirements and records itself as the task’s template.

One template per task. Once a template is applied, applying another (or the same one again) returns 400 (validation.task_already_has_template) unless you send "replace": true. replace deletes the requirements the previous template created, including any answers and photos on them, and then applies the new template. On a task shared with a partner company, replace returns 400 (validation.template_replace_shared_task_has_work) when the partner’s crew has already worked a requirement the swap would delete. Requirements added with addTaskRequirement, and issues reported by workers, are kept.

itemCount of 0 means the property has none of the elements the template targets. The task is still recorded as using that template, so you need replace to try a different one.

GET /task-requirements?taskId=… returns everything on one task. It isn’t paginated. getTask only gives completed and total counts.

Terminal window
curl "https://api-us.suiteop.com/api/v1/task-requirements?taskId=7d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a" \
-H "Authorization: Bearer sk_live_your_key_here"
  • data.items are the requirements, ordered by sortOrder. Each has isCompleted, answer, photos (download URLs), origin, and nested group, checklistItem and element objects.
  • origin is template, manager_added (created with addTaskRequirement) or worker_reported. Use origin rather than the older userCreated flag.
  • flag is text. On photo groups it holds the AI verifier’s reason. On every other type it holds the worker’s answer.
  • data.groups lists the task’s own requirement groups (the ones addTaskRequirement created), including groups that have no rows yet.
  • A task outside your property access returns 404.

A template requirement links to its template group and line instead of copying their text, so its group.title and checklistItem.description are read from the template each time you list. Rewording a template line with updateTemplateChecklistItem therefore changes it on tasks that already have it. New or moved template lines don’t reach those tasks until you apply the template again with replace.

To complete a task, every requirement must be satisfied. See Tasks.

POST /task-requirements adds one requirement to an existing task that isn’t completed or cancelled. Each call creates its own group, titled with description.

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/task-requirements \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b" \
-H "Content-Type: application/json" \
-d '{
"taskId": "7d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a",
"type": "checklist",
"description": "Balcony",
"checklistItems": ["Sweep floor", "Wipe railing"]
}'

The 201 response’s data is { "groupId": "…", "itemIds": ["…", "…"] }, with one item ID per checklist label and a single ID for every other type. The new rows go after the task’s existing requirements.

FieldNotes
typeyes_no (default), single_choice, checklist, photo, count, text or star_rating. See Choice types
yesNoOnly with type: "yes_no": which answer is a Fail and whether N/A is offered. Defaults to an information-only question with no N/A
descriptionShort label, 1 to 500 characters after trimming. Defaults to the type’s own name
checklistItems1 to 50 labels of up to 500 characters. Required for checklist (400 validation.checklist_items_required without it). Ignored for other types
optionSetIdThe answer list for single_choice, an ID from listAnswerLists. Leave it out for the Item Condition Check list. Ignored for every other type, including yes_no
elementInstanceIdAttaches the requirement to one element. The element must belong to the task’s property, or the call returns 404
elementCategorygeneral, arrival or completion. Attaches the requirement to the property’s element of that category, creating it if it’s missing. Takes precedence over elementInstanceId, and is ignored on a task with no property
photoAiVerificationPrompt1 to 500 characters. Only allowed with type: "photo" and without groupId, 400 otherwise
allowVideoSet true to let a photo requirement accept a video as well as photos. Defaults to false. Photo type only, not with groupId, and never with photoAiVerificationPrompt
groupIdAdds one more label (description, required here) to an ad-hoc checklist group on the same task — the groupId an earlier call returned, or a group from listTaskRequirements. Other type fields are ignored

PATCH /task-requirements/{id} changes the label only. Its body is flat, with no data wrapper:

Terminal window
curl -X PATCH "https://api-us.suiteop.com/api/v1/task-requirements/2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e" \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"description": "Sweep and mop floor"}'
  • description is required, 1 to 500 characters after trimming. The type, group and answer can’t be changed.
  • A requirement with origin: "template" is refused with 400 (validation.task_item_not_editable). Change its wording on the template instead.
  • The task must still be open (400 validation.task_must_be_open_to_edit otherwise).
  • The response’s data is { "id": "…" } only. Call listTaskRequirements to read the saved row.

This script builds a two-group template and applies it to a task that already has a property:

const BASE = 'https://api-us.suiteop.com/api/v1'
const headers = {
Authorization: `Bearer ${process.env.SUITEOP_API_KEY}`,
'Content-Type': 'application/json',
}
async function post(path: string, body: unknown) {
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify(body),
})
const json = await res.json()
if (!res.ok) throw new Error(`${res.status} ${json.error.message}`)
return json.data
}
const template = await post('/task-templates', {
name: 'Standard turnover clean',
departmentId: '0b6f2c1e-4d3a-4b2c-9e8f-7a6b5c4d3e2f',
})
const general = await post('/task-template-groups', {
templateId: template.id,
title: 'Every stay',
type: 'checklist',
rank: 0,
})
const lines = ['Strip and remake all beds', 'Empty all bins', 'Restock toiletries']
for (const [rank, description] of lines.entries()) {
await post('/task-template-checklist-items', { groupId: general.id, description, rank })
}
await post('/task-template-groups', {
templateId: template.id,
title: 'Final photo',
type: 'photo',
photoRequired: true,
photoRequirement: 'camera_only',
photoAiVerificationPrompt: 'the living room is tidy',
rank: 1,
})
const applied = await post('/task-template-applications', {
taskId: '7d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a',
templateId: template.id,
})
console.log(applied.itemCount) // 4: three checklist lines and one photo requirement