Housekeeping tasks and maintenance tickets
The workflow contract of housekeeping work and maintenance tickets. The decisions and their
reasons are in ADR 0046; room blocks
are in ADR 0033 and the room status itself in
reservations. Every path is under /api/v1/properties/{property_id} and
answers in the workspace of the GuestCraft-Workspace-Id header.
Who can do what
Section titled “Who can do what”| Action | Permission | Built-in roles |
|---|---|---|
| Read tasks, generated days and settings | inventory.read |
every role |
| Start, finish, skip and reopen tasks; edit a task’s note | housekeeping.update |
administrators, front desk, housekeeping |
| Read rosters, credits, checklist templates, a task’s checklist and the day’s automatic assignments | inventory.read |
every role |
| Answer a task’s checklist items | housekeeping.update |
administrators, front desk, housekeeping |
| Generate a day, create manual tasks, assign and reassign, change due date, priority and pin, plan a day’s roster, preview, apply and undo automatic assignment | housekeeping.assign |
administrators, front desk, housekeeping |
| Change the property’s housekeeping settings, credits and checklist templates | housekeeping.configure |
administrators |
| Read tickets, their activity and photos | maintenance.read |
every role |
| Report faults, edit, assign, change status, add and remove photos | maintenance.manage |
administrators, front desk, housekeeping |
| Put the room out of order or out of service with a report | blocks.manage_ooo or blocks.manage_oos |
as for blocks |
A task can be given only to a member who holds housekeeping.update at that property now; a
ticket only to one who holds maintenance.manage there (housekeeping.assignee_invalid,
ticket.assignee_invalid). GET /housekeeping_assignees and GET /maintenance_assignees
list them. Members limited to some properties see and assign only theirs.
Settings
Section titled “Settings”GET /housekeeping_settings answers {stayover, stayover_every_nights, inspection_required, version} with its ETag; a property that saved none has stayover service off, no inspection and
version 1. PUT /housekeeping_settings takes the ETag in If-Match.
stayover:off,dailyorevery_n_nightswithstayover_every_nightsfrom 2 to 30, counted from the arrival (every 3 nights: the third, sixth… day of the stay).inspection_required: a finished clean leaves the roomcleanand creates an inspection task; only aninspectedroom is ready for check-in. Without it,cleanis ready.
Saving re-synchronizes the generated days from the business date on.
| Type | Made by | Room | Due |
|---|---|---|---|
departure_clean |
a stay that leaves | the room of its last night | departure date, or the check-out day after an early departure |
move_clean |
a planned room move, also a future one | the room the guest leaves | the first night in the new room |
stayover |
the stayover setting | the room of that night | each service day of the stay, never the arrival day or a move day |
clean, deep_clean, inspection, turndown, other |
a person (POST /housekeeping_tasks) |
any active room | business date to one year ahead |
inspection |
a finished clean when inspection is required | the cleaned room | the clean’s day, or the business date for an overdue clean |
clean or inspection |
a closed maintenance ticket | the ticket’s room | the business date |
Statuses: pending, in_progress, done, skipped (with a reason such as “Do not
disturb”) and cancelled. People move tasks freely between the first four with
PUT /housekeeping_tasks/{id}/status and the task’s ETag, so Undo is the inverse change;
cancelled belongs to generation and refuses changes (housekeeping.invalid_state).
Effects on the room status:
- finishing a clean sets the room
clean(and creates the inspection when required); - finishing an inspection sets it
inspected; - reopening a finished task restores the room’s previous status if the room still has the status the task gave it, and withdraws its pending inspection;
- skipping, starting and turndown or other tasks leave the room as it is.
Generation
Section titled “Generation”POST /housekeeping_days with {date} generates a day, from the business date to 30 days
after it: 201 the first time, 200 when run again. It is idempotent: a second run adds only what
the stays now owe and cancels pending tasks they no longer owe. Each run gives the day its next
version, answered as its ETag. GET /housekeeping_days/{date} answers a generated day with when
it was first generated and its version; a day never generated answers 404
housekeeping.day_not_found, which the board shows as “not generated yet”.
After that, every reservation change reschedules the reservation’s tasks in the same transaction, whoever made it (front desk, planning, policies, OTA import):
- a new date or room moves the pending task (same task, new
due_onorunit_id); - a shortened stay, an undone move, a cancellation or a no-show cancels its pending tasks;
- tasks that have started, finished or were skipped are never changed;
- a new task appears only on a generated day.
The database refuses a second task for the same stay fact (origin_key, unique per property).
Lists and bulk changes
Section titled “Lists and bulk changes”GET /housekeeping_tasks?from=YYYY-MM-DD[&to=…] lists up to 31 days (default: one day), with
the filters status (comma-separated), type, assignee_id (usr_… or me),
unassigned=true and unit_id; without status, every status is listed, cancelled tasks
included. Tasks come in board order: due date, arrivals first (a guest is
expected in the room that day: arrival_expected), priority (high, normal, low), then the
room order.
POST /housekeeping_task_status_changes and POST /housekeeping_task_assignments change 1 to
100 distinct tasks, each with the version the client read instead of an If-Match, all or none:
one missing task, stale version (412) or refused transition changes nothing. A repeated request
after a success finds the new versions and answers 412.
Creation accepts a client id (hkt_…): repeating the request answers the same task with 200
instead of creating another, so an offline client can resend safely. An id that a task of
another property already uses is refused (housekeeping.invalid, field id) and changes nothing.
Maintenance tickets
Section titled “Maintenance tickets”/maintenance_tickets holds faults of one room: title, notes, priority (low, normal,
high, urgent), assignee, scheduled_on, due_on, the reporting member, the task it was
found during (source_task_id) and a linked block.
- Report a fault:
POST /maintenance_tickets, optionally withblock: {kind, from, to, reason}to put the room out of order or out of service in the same transaction under the block rules, and with a clientid(mtk_…) for safe resending (theidof another property’s ticket isticket.invalid, fieldid; nothing is written). A report that creates a block also answersoverbooked_dates, the nights it oversells the room type, as creating a block does. - Edit:
PATCH /maintenance_tickets/{id}with the ETag. - Status:
PUT /maintenance_tickets/{id}/statusmoves betweenopen,in_progressanddone. Closing requiresfollow_up: "clean" | "inspection", which creates that housekeeping task for the room; the room status never changes, as the analysis requires. Reopening withdraws the follow-up while it is pending. - Activity:
GET /maintenance_tickets/{id}/activity, oldest first: who did what and when, never the text.
Photos
Section titled “Photos”POST /maintenance_tickets/{id}/photos takes {id?, content_type, base64}: a JPEG or PNG of at
most 2,000,000 bytes (photo.too_large, also for base64 longer than such a photo’s), in a request
body of at most 3 MiB, 10 photos per ticket. The server checks that the bytes are a structurally valid
image of the declared type, by its grammar, without decoding its compressed data (ADR 0046 lists
every rule) (photo.invalid, field base64; an id
another property’s photo uses is photo.invalid, field id), refuses a picture of more than 4096
pixels on a side or 16 megapixels (photo.dimensions_too_large), and removes location and other metadata
(Exif, XMP, IPTC, comments, PNG text chunks) before storing it. Clients downscale before sending;
the web and mobile apps send a JPEG of at most 1,600 pixels.
GET /maintenance_tickets/{id}/photos/{photo_id} answers the image with
Cache-Control: private, max-age=300, X-Content-Type-Options: nosniff,
Content-Disposition: inline and ETag: "1" (a photo never changes), to members who can read the
ticket’s property.
The photos are part of the ticket: adding a new one (not a repeated upload) or removing one gives
the ticket its next version, which both answer as ETag. DELETE removes a photo and takes the
ticket’s ETag in If-Match (ticket.version_mismatch, 412, with the current version).
Photos are stored files behind the FileStore port: in the database (stored_files) by default,
or in an instance’s object storage (an S3-compatible bucket or a private Vercel Blob store) when it
configures one. Downloads always go through this route and its permission check. A downscaled
photo takes 150–400 kB, and free database tiers hold about 500 MB in total: see the
installation guide and its object storage section.
Planning a day
Section titled “Planning a day”A room’s floor is its section for housekeeping (the screens call it “Floor or section”).
GET/PUT /housekeeping_rosters/{date}: the members who work on a day,{staff: [{user_id, starts_at, capacity_credits, section}], version}with its ETag; a day nobody planned answers no staff at version 1.PUTtakes the ETag inIf-Matchand replaces the list: at most 100 distinct members who can be given tasks there now (housekeeping.assignee_invalid), each with a start time (HH:MM), a capacity of 1 to 999 credits and an optional section (a floor or section name). Days from the business date to 30 days after it (housekeeping.invalid, fielddate).GET/PUT /housekeeping_credits:{rules: [{task_type, room_type_id, credits}], version}, how many credits (0 to 100) a task type takes in rooms of a room type, or of any room type whenroom_type_idis null; at most 200 rules, each pair once. A type without a rule takes its default:departure_cleanandmove_clean3,deep_clean5,clean2, the others 1.
Automatic assignment
Section titled “Automatic assignment”GET /housekeeping_assignment_plans/{date} previews, without writing, how the day’s work would be
distributed over its roster:
changes: each task that would get a new assignee, with its version, from whom and to whom, and its credits;unassigned: each task nobody has room for, with the reasonno_staff(nobody available on the roster) orno_capacity; a task automatic assignment gave earlier is also inchanges, taken back to nobody (to_assignee_id: null);staff: per member of the roster, whether they can still be given tasks there (available; an unavailable member gets nothing), and the credits of the work they keep and of the work planned;basis: what applying would write, to send back when applying.
Automatic assignment moves only pending tasks that are not pinned and are unassigned or assigned automatically. A person’s assignment, a pinned task and work already begun stay as they are; their credits count against their assignee. Each movable task, arrivals first, goes to a member with enough capacity left: one of its section first, then one without a section, preferring a section the member already works in, the earliest start for an arrival room, and the lightest load. The same tasks, roster and credits always give the same plan.
POST /housekeeping_assignment_runs with {date, basis} applies the previewed plan in one audited
operation and answers the run (201): its changes with each task’s previous assignee. A plan that
changed since the preview answers housekeeping.plan_changed (409): preview again. A plan with no
change answers housekeeping.plan_empty (409).
POST /housekeeping_assignment_runs/{id}/undo restores every task of the run to its previous
assignee, exactly, or nothing: it answers housekeeping.undo_conflict (409) when the run was
undone already, when a task of it changed since, or when a previous assignee can no longer be given
tasks there. GET /housekeeping_assignment_runs?date= lists the day’s runs, newest first, and
GET /housekeeping_assignment_runs/{id} one run.
Assigning a task by hand, also to its current assignee, makes it a manual assignment that automatic
assignment keeps. PATCH /housekeeping_tasks/{id} with pinned: true keeps a task as it is,
assigned or not.
Checklists
Section titled “Checklists”/housekeeping_checklists holds the property’s templates: {name, task_type, room_type_id, items: [{key, label, kind, required}], version}. A template applies to tasks of its type in rooms of its
room type, or of any room type when room_type_id is null; a property has one template per pair
(housekeeping.checklist_duplicate, 409). Items are 1 to 50, each with a key (a-z, 0-9, _,
up to 40, unique in the template), a label of up to 200 characters, a kind (check: ticked when
done; inspection: passed or failed) and whether it is required. PUT replaces the name and items
as the next version, DELETE removes the template; both take its ETag. The task type and room type
of a template do not change.
When a task is created, the template that applies then is copied onto it: its checklist says
which version, with total, answered and required_open items. Later edits never change the
tasks that exist.
GET /housekeeping_tasks/{id}/checkslists the task’s items with their answers.PUT /housekeeping_tasks/{id}/checks/{key}with the task’s ETag answers one:checked(a check item),passedorfailed(an inspection item), ornullto clear it (housekeeping.invalid, fieldresult), only while the task is pending or in progress (housekeeping.invalid_state). A failed item without follow-up work needsfollow_up:{kind: "task", type, note}(clean, deep clean, inspection or other, due on the task’s day or the business date if later) or{kind: "fault", title, priority}(a maintenance ticket found during the task, which also needsmaintenance.manage). The follow-up is created in the same transaction and stays when the answer changes later. The task gets its next version.- A task whose required items are not all answered cannot be done
(
housekeeping.checklist_incomplete, 409), alone or in a bulk change. A failed item is answered.
Webhook events
Section titled “Webhook events”Tasks publish three events of the catalog, with the object housekeeping_task (the task, its
property and, for a task a stay made, its reservation):
| Event | When |
|---|---|
housekeeping_task.created@1 |
a task is created: generated, by a person, an inspection after a clean, a closed ticket or a failed checklist item |
housekeeping_task.assigned@1 |
its assignee changes: by a person, in bulk, by automatic assignment or its Undo; also a task a person creates for a member, after its created |
housekeeping_task.status_changed@1 |
its status changes, also a cancellation or reinstatement that follows its stay |
A task moved to another date or room by its stay keeps its status and publishes none of them.
Not included
Section titled “Not included”Push notifications and checklists in the mobile app are not included. Offline, the mobile app queues task status, notes and fault reports with their photos in the outbox of its offline layer and sends them on reconnection (ADR 0041, ADR 0046).