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.
| Operation | Request | Permission |
|---|---|---|
listTemplates | GET /task-templates | view_task_templates |
getTemplate | GET /task-templates/{id} | view_task_templates |
createTemplate | POST /task-templates | manage_task_templates |
updateTemplate | PATCH /task-templates/{id} | manage_task_templates |
addTemplateGroup | POST /task-template-groups | manage_task_templates |
updateTemplateGroup | PATCH /task-template-groups/{id} | manage_task_templates |
addTemplateChecklistItem | POST /task-template-checklist-items | manage_task_templates |
updateTemplateChecklistItem | PATCH /task-template-checklist-items/{id} | manage_task_templates |
applyTemplateToTask | POST /task-template-applications | modify_tasks |
listTaskRequirements | GET /task-requirements?taskId={taskId} | view_tasks |
addTaskRequirement | POST /task-requirements | modify_tasks |
updateTaskRequirement | PATCH /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.
Listing templates
Section titled “Listing templates”GET /task-templates is paginated (default limit 20, maximum 100). Rows are ordered oldest first.
curl "https://api-us.suiteop.com/api/v1/task-templates?nameContains=turnover&limit=50" \ -H "Authorization: Bearer sk_live_your_key_here"nameContainsis a case-insensitive substring, at most 100 characters.%and_match literally, not as wildcards. It matches the stored name and the name’s translation inlanguage.departmentIdkeeps the templates owned by one department.includeDeleted=truealso returns templates deleted in the app. TheirdeletedAtis set.language(for examplees) chooses the language ofname. 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.
Reading one template
Section titled “Reading one template”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.
Creating a template
Section titled “Creating a template”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.
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-…" }}nameanddepartmentIdare 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.departmentIdmust be an active department in your organization (uselistDepartments). An archived department or one from another organization returns404.priorityis the default priority for the template’s tasks:watch,low,medium,highorurgent. It defaults tomedium.requiredSkillIdis a skill a member needs before a task from this template can be assigned to them. No operation lists skills, butgetMemberreturns one member’s skills with their IDs. An ID from another organization returns404.isAdminTemplate: truehides the template from the pickers ordinary staff see. It defaults tofalse.languageis the languagenameanddescriptionare written in. It defaults toen.
Updating a template
Section titled “Updating a template”PATCH /task-templates/{id} takes a data object. Fields you leave out keep their current value.
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,requiredSkillIdandisAdminTemplate. description: nullremoves the description, andrequiredSkillId: nullremoves the skill requirement.departmentIdcan be changed to another department but can’t be cleared.datamust name at least one field. An emptydata, or one with onlylanguage, returns400 validation_error, and itsdetails.issuesentry saysAt least one field to change must be provided.languagesets which language a newnameordescriptionis written in (defaulten).- 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.
Groups and checklist lines
Section titled “Groups and checklist lines”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.
Choice types
Section titled “Choice types”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.
Adding a group
Section titled “Adding a group”POST /task-template-groups takes the templateId in the body.
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.
| Field | Notes |
|---|---|
title | Required. Trimmed, and must not be blank. Written in language (default en) |
targetCategoryId | An 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 |
targetLevel | area or item. Only valid together with targetCategoryId |
parentCategoryId | Limits 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) |
type | Leave it out for an untyped group |
rank | Position 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 |
photoRequired | Defaults to false |
photoRequirement | disabled (default), camera_only or camera_and_file |
photoAiVerificationPrompt | What the AI verifier checks the photo against, for example the bed is made. Only checked on photo groups. A blank string means no prompt |
allowVideo | Set 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 |
optionSetId | The 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, flagRequiresIssue | single_choice only: what a Fail or Flag answer demands. Left out, copied from the answer list. See Choice types |
yesNo | Only 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.
Adding checklist lines
Section titled “Adding checklist lines”POST /task-template-checklist-items adds one line per call. Make one call for each line.
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 }'groupIdis a group ID fromaddTemplateGrouporgetTemplate, not the template ID.descriptionis required. It’s trimmed and must not be blank.rankdefaults to1, so send it whenever the order matters.inventoryCatalogIdlinks 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 acountgroup, or the call returns400. 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.
Updating groups and lines
Section titled “Updating groups and lines”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.
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
targetLevelandparentCategoryIdrules are checked against the fields you send, not against the stored group. AtargetLevelsent withouttargetCategoryIdis rejected even if the group already targets a category. targetCategoryId: nullturns the group into a general section and also clears its storedtargetLevelandparentCategoryId.allowVideo: truelets aphotogroup 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 sendsallowVideo: false, and retyping the group away fromphototurns it off.photoAiVerificationPrompt: nullor""removes the prompt.optionSetId: nullon asingle_choicegroup rebinds it to the Item Condition Check list. Sent withouttype, a non-null answer list on any other group is refused (400).- Changing the
typeof a group whose lines are linked to inventory to anything other thancountreturns400(validation.inventory_binding_requires_count_group).
Line rules:
groupIdmoves the line to another group on the same template. A group on a different template returns400(validation.checklist_item_move_same_template). An archived or unknown group returns404.inventoryCatalogId: nullremoves the inventory link. A new link needs acountgroup, including after a move.
Applying a template to a task
Section titled “Applying a template to a task”createTask doesn’t accept a templateId over REST (see Tasks). Create the task, then apply the template with POST /task-template-applications:
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’srank. 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.
Task requirements
Section titled “Task requirements”Listing
Section titled “Listing”GET /task-requirements?taskId=… returns everything on one task. It isn’t paginated. getTask only gives completed and total counts.
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.itemsare the requirements, ordered bysortOrder. Each hasisCompleted,answer,photos(download URLs),origin, and nestedgroup,checklistItemandelementobjects.originistemplate,manager_added(created withaddTaskRequirement) orworker_reported. Useoriginrather than the olderuserCreatedflag.flagis text. Onphotogroups it holds the AI verifier’s reason. On every other type it holds the worker’s answer.data.groupslists the task’s own requirement groups (the onesaddTaskRequirementcreated), 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.
Adding a one-off requirement
Section titled “Adding a one-off requirement”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.
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.
| Field | Notes |
|---|---|
type | yes_no (default), single_choice, checklist, photo, count, text or star_rating. See Choice types |
yesNo | Only 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 |
description | Short label, 1 to 500 characters after trimming. Defaults to the type’s own name |
checklistItems | 1 to 50 labels of up to 500 characters. Required for checklist (400 validation.checklist_items_required without it). Ignored for other types |
optionSetId | The 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 |
elementInstanceId | Attaches the requirement to one element. The element must belong to the task’s property, or the call returns 404 |
elementCategory | general, 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 |
photoAiVerificationPrompt | 1 to 500 characters. Only allowed with type: "photo" and without groupId, 400 otherwise |
allowVideo | Set 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 |
groupId | Adds 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 |
Rewording a requirement
Section titled “Rewording a requirement”PATCH /task-requirements/{id} changes the label only. Its body is flat, with no data wrapper:
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"}'descriptionis required, 1 to 500 characters after trimming. The type, group and answer can’t be changed.- A requirement with
origin: "template"is refused with400(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_editotherwise). - The response’s
datais{ "id": "…" }only. CalllistTaskRequirementsto read the saved row.
End-to-end example
Section titled “End-to-end example”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