18 · Work and intake — one inbox, one owner
The owner's requirement (2026-09-23): no matter where a lead, sale or request comes from — website, WhatsApp, email, the phone, the counter, a sub-agent — it is listed in one place for a person to work on. This file turns that into rules.
Before this, work arrived in six different places and the system had no table for a task at all. The "inbox" was read-only SQL over business tables (dash_approvals_inbox and friends), keyed off permissions rather than assignment, so no screen could say who owed what by when.
Related: 17-intelligence.md (INT-003 suggest-never-act, INT-004 explain every signal, INT-005 one computation), 08-access-control.md (ACC-001), 09-audit-and-data-protection.md, and 19-performance.md, which measures the handling this file records.
Principles
WRK-001 · One authoritative owner for a piece of work
Status: DECIDED 2026-09-23 (owner) · Owner: Management
A piece of work has exactly one owner, in one place. WorkItem is authoritative only where nothing owns the work today: arrivals from any channel, typed tasks, lead follow-ups and posting failures. Work that already has an owner keeps it and is shown in the inbox as a read-only projection:
| Already owned | Where the owner lives |
|---|---|
| Booking correction | Booking."correctionOwnerId" + "correctionDueAt" |
| Incident | Incident."ownerId" via assign_incident_owner() |
| Customer request | CustomerRequest."assignedToId" |
| Visa enquiry | VisaIntakeRequest."assignedToId" |
| Group | TravelGroup."leadUserId" |
| Inventory hold | InventoryHold."holderUserId" |
A projected row is not assignable or closable from the inbox — it links to the screen that owns it. Copying these into a second table was rejected deliberately: four lifecycle paths set correctionOwnerId to NULL unconditionally, and CustomerRequest."assignedToId" is written by a raw table update with no RPC guard, so a mirror would drift with no scheduler available to reconcile it.
Enforced by: work_inbox_rows() in 20260926220000_work_inbox.sql; test supabase/tests/work_inbox_paging.sql.
WRK-002 · Every arrival keeps its real record
Status: DECIDED 2026-09-23 (owner) · Owner: Operations
An enquiry still creates its Lead, a visa enquiry still creates its VisaIntakeRequest. The work item points at that record (subjectType/subjectId); it never becomes a second copy of the customer or the enquiry. A typed task has no subject and stands alone.
An arrival raises its item by itself: a new Lead (website, phone, walk-in, sub-agent, email, WhatsApp by source) and an inbound WhatsApp message each raise an enquiry item in the sales pool. A lead moved on from new, or a staff WhatsApp reply, records the first response; a lead converted, lost or closed closes its item. If the item cannot be made, the arrival is still saved and the database logs a warning (PLT-033).
Note (2026-10-01, COMM-030): with the WhatsApp menu switched on, an inbound message the menu answered, and the menu's own replies, raise no item — a booking enquiry made in the menu arrives as a WhatsApp Lead instead, and Talk to the office or a bank receipt raises the conversation's item through whatsapp_menu_handoff().
Enforced by: work_item_create() / work_item_create_as(); the triggers work_on_lead_arrival, work_on_lead_status, work_on_whatsapp_message; the same-subject guard below; test supabase/tests/every_arrival_is_work.sql.
WRK-003 · Response targets are set per channel
Status: DECIDED 2026-09-23 (owner) · Owner: Management
First-response targets, in working minutes: WhatsApp 30, phone 60, website 120, email 240. Walk-in 60, sub-agent 120, internal 480. A work type may set its own target where no channel applies. Targets are settings (WorkChannelTarget), not code.
Enforced by: WorkChannelTarget; work_due_at(); test supabase/tests/work_clock.sql.
WRK-004 · Every clock is a business clock
Status: DECIDED 2026-09-23 (owner) · Owner: Management
The working week is Monday–Saturday 10:00–19:00 IST, with the Friday prayer break 12:30–14:00 and a holiday list. Sunday is closed. No elapsed time in this system is measured in wall-clock hours: an enquiry arriving at 22:00 is not late at 09:00 the next morning, and nobody is charged for the night.
Enforced by: BusinessCalendar, BusinessCalendarException, work_business_minutes(); test supabase/tests/work_clock.sql.
WRK-005 · The clock stops while waiting on somebody else
Status: DECIDED 2026-09-23 (owner) · Owner: Management
When an item is waiting on a customer, a supplier, an embassy or another department, the handler says who it is waiting on and why, and the clock stops until it restarts. Waiting time is excluded from every elapsed figure. A person is never measured on somebody else's silence — and because the reason is recorded, "waiting" cannot be used to hide a slow item.
Enforced by: work_item_wait() / work_item_resume(), work_waiting_minutes().
WRK-006 · A pool item is claimed atomically; a release is recorded
Status: DECIDED 2026-09-23 (owner) · Owner: Operations
Work either goes to a named person or waits in a queue anyone permitted may claim. Two people claiming at the same instant: one gets it, the other is told it has gone — never both. Putting work back requires a reason and is recorded, because dropping work is a coaching signal, not a non-event.
Enforced by: work_item_claim() (UPDATE … WHERE "assigneeId" IS NULL), work_item_release(); test supabase/tests/work_items.sql.
WRK-007 · Work goes to its person by rule; otherwise a person decides
Status: DECIDED 2026-09-23, amended 2026-09-24 (owner) · Owner: Management
- Routing rules. A manager (work.assign) can set, per kind of job, where new jobs go: a named person, or someone holding a role, least busy first (fewest open items, then whoever was given work longest ago). A new job of that kind is assigned as it arrives, and the ledger names the rule. A kind of job with no rule waits in the pool. So does one whose person can't receive work right now (inactive, or no access to the inbox). Setting a rule needs a reason, and every change is audited. Changing a rule doesn't move jobs already given out.
- Handing on. The person holding a job can hand it to anyone, with a reason, if it isn't theirs to do. A manager can do this for any job. Nobody else can. The due time never moves (WRK-011).
- Suggestions. For a manager handing out a job, the system ranks candidates by load and says why (INT-003). It suggests. A person confirms.
Enforced by: WorkRoutingRule, work_route_assignee(), work_routing_set() (work.assign), work_item_create_as(), work_item_assign(), suggest_work_assignee(); test supabase/tests/work_goes_to_its_person.sql.
WRK-008 · Staff, tour leaders and partners may all receive work
Status: DECIDED 2026-09-23 (owner) · Owner: Operations
An assignee is a member of staff, a tour leader or a partner. Staff rank first in any suggestion. A work type must be explicitly flagged partnerAllowed before it can reach a partner, because partners and tour leaders sign in through portals that deliberately show only their own data. Portal delivery is a later phase; the model carries the distinction from the first migration so nothing has to be migrated later.
Enforced by: WorkItem."assigneeKind", WorkType."partnerAllowed".
WRK-009 · Reminders and escalation rungs
Status: DECIDED 2026-10-02 (owner) · Owner: Management The owner (02/10/2026): job notifications are reminders in the web and phone app, not only e-mail. Every reminder is a note in the person's notifications (the desktop bell, the phone app's inbox), a push to their phone, and an e-mail copy.
| When | Who is told |
|---|---|
| About one working hour before the due time (only for jobs with more than an hour of target) | the holder — Due soon |
| At the due time | the holder — Overdue |
| At 1.5× the target (half the target past the due time) | the queue's lead(s): WorkQueueMember with role lead on the job's queue, named in Work → Queues (WRK-013) — Overdue in your queue |
| At 2× the target (the whole target past the due time) | every active holder of work.assign — Overdue, twice its time — note and push, no e-mail |
| 24 working hours (1,440 working minutes) past the due time | every active holder of work.view.all — Overdue by a working day or more — note and push, no e-mail |
There is no manager or reporting hierarchy (WRK-013), so the last two rungs name permissions, not "their manager".
- Time is the business clock (WRK-004) less time spent waiting (WRK-005), measured from the due time, so a due time that is changed is respected. A job that is waiting on somebody else is skipped: its clock is stopped. A closed job is skipped.
- No catch-up flood. A rung tells people only when it was crossed within the last working hour. One crossed longer ago — the schedule was down, or a batch of jobs arrived already overdue — is marked done without a note (
WorkReminder.told = false), so a lapse never sends hundreds of notes and e-mails at once. - Each rung fires once per job and due time, and each person gets it once. A new due time re-arms every rung.
- A rung with nobody to tell (a job in the pool has no holder; a queue has no lead) is not spent: it fires when somebody can be told — within its hour; after that it is marked done like any rung long past. A person given an overdue job is told by the hand-over note, which names the due time.
- Jobs due more than 30 days ago are left alone.
- Only
WorkItemrows are reminded. A projected row (a booking correction, an incident, a customer request, a visa enquiry, a group, a hold — WRK-001) keeps its own screen and clock and is not reminded here.
Enforced by: work_reminders_run(p_now) (service role only, advisory-locked), run every 15 minutes by the pg_cron job alhuda-work-reminders; WorkReminder (unique per job, rung and due time; RLS on, no client access); work_tell(); work_waiting_minutes_at(); work_permission_holders(); all in 20261008150000_work_reminders.sql. The phone push is COMM-041. Test: supabase/tests/work_reminders.sql. Runbook: Work reminders.
WRK-010 · Acknowledgement is automatic unless switched off
Status: DECIDED 2026-09-23 (owner) · Owner: Management
An arrival is acknowledged automatically by email unless the switch is off for that channel; WhatsApp and SMS are available too. Every automatic message passes the consent gate (customer_contact_allowed, AUD-022) and goes through the existing CommunicationQueue → communications-dispatcher path — no second mail path is built.
Enforced by: WorkChannelTarget."ackEnabled"; phase 2. Not built yet: today nothing is sent automatically.
WRK-011 · Reassignment never moves the due time
Status: PROPOSED · Owner: Management
Handing an item to somebody else does not restart its clock. A genuine change of target is a separate, permissioned act with a reason, and the original breach stays on the record. Otherwise a late item can be made punctual by passing it around.
Enforced by: work_item_assign() (leaves dueAt untouched).
WRK-012 · A work item holds a pointer, not a person's file
Status: LAW (DPDP Act 2023) + PROPOSED detail · Owner: Management
A work item stores the link to the real record and the least contact detail needed to answer somebody who is not a customer yet (name, phone, email). It never accumulates passport numbers, payment details or health information. Minimisation, per INT-006.
Enforced by: the WorkItem column list; review.
WRK-013 · Queue membership is the only team model
Status: DECIDED 2026-09-23 · Owner: Management
This system has no manager, team, department or reporting hierarchy: User has no such column, and Department is unused. "The team" therefore means the members of a queue, and "their manager" means the member marked lead on that queue. Nothing else is invented. Until a queue has members, anyone permitted may claim from it, so a fresh deployment is never an inbox nobody can touch.
Members and leads are named in Work → Queues by a holder of work.queues.manage (CEO, GM): add a person who can see the work inbox, make them member or lead, set their weekly capacity points, take them off. Taking someone off keeps the row as the record; bringing them back starts from that day. Once a queue has a member, only its members may pick up its waiting jobs; once it has a lead, My queues shows it to its leads, and the lead is told at 1.5× the target (WRK-009). Weekly capacity points are recorded but nothing reads them yet.
A member must be able to take jobs (work.claim) and a lead must be able to see a queue (work.view.queue), each with an active, unpaused login (owner's reviewer, 02/10/2026). A member or lead whose login is switched off or paused no longer counts, so a queue is never locked to someone who cannot work it: with no member left who can, it opens to everyone again (work_is_queue_member, work_leads_queue, work_member_can_work, 20261008170000).
Enforced by: WorkQueueMember, work_is_queue_member(), work_leads_queue(); work_queue_member_save() / work_queue_member_remove() (work.queues.manage, checked in the function; audited by audit_table_change) and work_queue_members_list() in 20261008170000_work_queue_members.sql; test supabase/tests/work_queue_members.sql.
WRK-014 · How lateness is noticed
Status: DECIDED 2026-10-02 · Owner: Engineering
Lateness is (1) derived on read, so the list is always correct when somebody looks; and (2) swept every 15 minutes by the pg_cron job alhuda-work-reminders, which runs work_reminders_run() and tells people (WRK-009). The sweep is idempotent and advisory-locked, so two runs never fire one reminder twice. The CI schedule and the "heartbeat when somebody opens the inbox" proposed earlier are not built: pg_cron is available, so they are not needed.
Enforced by: 20261008150000_work_reminders.sql. Where pg_cron is missing the migration says so and succeeds; the sweep is then run by hand (runbook).
WRK-015 · The ledger is append-only
Status: DECIDED · Owner: Engineering
Every change to a work item writes an event: created, assigned, reassigned, claimed, released, first response, waiting started and ended, escalated, reopened, sent back, closed. Those rows are never updated or deleted — a correction is another event. This is what makes a performance number traceable (PERF-005), and it is stronger than an audit trigger, which is why WorkItemEvent and WorkTypeWeight are the two deliberate exceptions to audit_table_change().
Enforced by: work_append_only_guard(); test supabase/tests/work_clock.sql.
WRK-016 · One open item per subject
Status: DECIDED · Owner: Engineering
The same subject and type may have only one open work item. A repeated intake or a repeating signal returns the item that already exists rather than raising a second one. Without this, a nightly sweep would raise the same job every night and one job would be counted many times.
Typed tasks (task_general, typed by a person) are the exception: a person may type several tasks about one booking, and each is its own job (WRK-018).
Enforced by: work_item_create(); the partial unique index on idempotencyKey.
WRK-017 · An email to info@ becomes a lead
Status: DECIDED 2026-09-24 (owner) · Owner: Sales
Email to info@alhudatravels.in becomes a Lead (source email), and through WRK-002 an enquiry in the Sales pool, due by the email target. Each message is recorded once, by its Message-ID. A reply in a known thread, or another email from a sender whose enquiry is still open (within 30 days), joins that lead instead of making a second one. Mail from our own domains, automated senders, auto-replies and bulk mail is skipped and recorded with the reason. Leads only: an email never creates a customer or a booking. Staff convert the lead as usual.
Enforced by: email-intake edge function (shared secret); email_intake_record() (service role only); InboundEmail; test supabase/tests/an_email_becomes_a_lead.sql. Installed and run as in Email to info@. Not done: replies to the sender (WRK-010), attachments, and other addresses. alhuda.co.in mail is on Yandex, not Google, so this method cannot read it.
WRK-018 · A typed task is a job like any other
Status: DECIDED 2026-09-27 (owner) · the default target for a task with no due time is PROPOSED · Owner: Management
The owner asked (2026-09-27): create and assign tasks, and performance should be calculated on those as well. A task a person types (task_general) is measured exactly like an arrival: its first answer, its finish and points, finishing late, a reopen, and being open or overdue now (19-performance.md). So a task always has a clock:
- A due time. Whoever types the task may set one. It must be in the future. The target becomes the working minutes from now until then, so "first answer within target", "finished late" and "overdue" all measure against it.
- No due time given: the internal target, 480 working minutes (about one working day). Proposed: the owner may prefer no clock at all for such tasks; they would then never be late.
- A priority: Urgent, High, Normal, Low. It orders nothing by itself and is not a performance figure.
- A link to a record: a booking, a customer, a group or a partner. The record must exist; several tasks may point at one (WRK-016 exception).
- Who holds it: the person typing it may keep it (with work.claim) or leave it in the pool. Giving it to someone else needs work.assign (WRK-007).
- The new holder is told. Every assignment or hand-over to someone other than the person doing it writes one note in the holder's notifications, rings their phone and sends an e-mail copy. Claiming a job yourself tells nobody. Amended 2026-10-02 (owner): the phone is rung from the server (COMM-041), not by the browser or app that made the hand-over, and the e-mail copy is new (category work, COMM-039).
- The person who gave the job is told when it closes (owner, 2026-10-02): Finished or Cancelled, with the closing line — a note, the phone and an e-mail. "Who gave it" is whoever handed it to its last holder, or, for a job a routing rule gave out or nobody held, the person who typed it. Nobody is told about a job they closed themself or gave themself. A job the system cancels as bookkeeping (the other admins' copies of a role change, ACC-077) tells nobody.
- Reminders: due soon and overdue — WRK-009.
Before this, task_general had no target and a task no channel, so a typed task could never be late or overdue, and its first answer was always "on time".
Enforced by: work_item_create_as(), trigger WorkItemEvent_notify_new_holder (work_notify_new_holder()), work_people(), work_given_by_me() in 20261001170000_tasks_count_in_the_scorecard.sql; test supabase/tests/tasks_count_in_the_scorecard.sql. The notes go through work_tell(); the close note is the trigger WorkItemEvent_notify_giver (work_notify_giver()), both in 20261008150000_work_reminders.sql; test supabase/tests/work_reminders.sql.
Not built: a checklist inside a task, repeating tasks, and changing a task's due time after it is created (WRK-011 says that needs its own permissioned act).
What is not built yet
Said plainly, because silence reads as if it works:
- Nothing receives email. There is no inbound mail address. Email arrives as work only once phase 2 adds it.
- Nothing is acknowledged automatically yet (WRK-010) — the switch exists, the sender does not.
- Reminders cover work items only (WRK-009). A late booking correction, incident, customer request, visa enquiry, group or hold is shown as late in the inbox, and its own screen keeps its own clock, but nobody is reminded from here. Reminders cannot be switched off per person; the e-mail copies can be switched off as a whole (category
work). - No SMS sender exists. MSG91 credentials are configured; no code sends through them.
- Tour leaders and partners cannot yet receive work (WRK-008) — the model allows it, the portals do not show it, and there is no
TOUR_LEADERrole.