{"spec":"AIP-BS-EXCEPTION-DESK","version":"2026-11-01","standard":"exception-desk-build-standard","source":"https://aipathway.com.au/explore-ai/exception-desk-build-standard","markdown":"https://aipathway.com.au/explore-ai/exception-desk-build-standard.md","licence":"CC BY 4.0","how_to_read":"Eight of these nine checks run on your own machine, against the stub, before you talk to anybody. The last one is a live tenant, and that one is quoted as a scoped build.","verdicts":{"pass":"asserted green here","fail":"asserted red here. This is a fixture: it names the check to price","shape_only":"shape holds on the stub, truth needs the live provider","with_us":"cannot be proved on the stub; the live step is quoted as a scoped build (see with_us_next)","evidence_required":"not a software check. Attach what proves it"},"tally":{"total":9,"onStub":8,"productionOnly":1,"partial":2,"preconditions":0},"envelope_version":"1.0","port_shape":{"read_ledger":{"verb":"read","subject":"ledger"},"read_job":{"verb":"read","subject":"job"},"read_asset":{"verb":"read","subject":"asset"},"read_rule":{"verb":"read","subject":"rule"},"read_contacts":{"verb":"read","subject":"contacts"},"read_segment":{"verb":"read","subject":"segment"},"resolve_address":{"verb":"read","subject":"address"},"write_job":{"verb":"write","subject":"job"},"read_pricebook":{"verb":"read","subject":"pricebook"},"draft_quote":{"verb":"write","subject":"quote"},"write_invoice":{"verb":"write","subject":"invoice"},"write_ledger":{"verb":"write","subject":"ledger"},"record_promise":{"verb":"write","subject":"promise"},"record_service":{"verb":"write","subject":"service_record"},"release_payment":{"verb":"write","subject":"payment"},"talk":{"verb":"say","subject":"turn"},"disclose":{"verb":"say","subject":"disclosure"},"send_sms":{"verb":"say","subject":"sms"},"notify":{"verb":"say","subject":"alert"},"escalate":{"verb":"say","subject":"handoff"},"issue_notice":{"verb":"say","subject":"notice"},"answer":{"verb":"say","subject":"answer"},"publish_report":{"verb":"say","subject":"report"},"record_consent":{"verb":"write","subject":"consent"},"read_file":{"verb":"read","subject":"file"},"write_file":{"verb":"write","subject":"file"},"raise_task":{"verb":"write","subject":"task"},"read_node":{"verb":"read","subject":"node"},"write_node":{"verb":"write","subject":"node"},"read_register":{"verb":"read","subject":"register"},"write_register":{"verb":"write","subject":"register"},"index_passage":{"verb":"attach","subject":"passage"},"search_index":{"verb":"read","subject":"index"},"read_inbox":{"verb":"read","subject":"message"},"file_message":{"verb":"attach","subject":"message"},"write_order":{"verb":"write","subject":"order"},"read_quote":{"verb":"read","subject":"quote"},"write_log":{"verb":"write","subject":"log"},"read_sales":{"verb":"read","subject":"sales"},"read_sale":{"verb":"read","subject":"sale"},"draft_message":{"verb":"write","subject":"draft"},"send_message":{"verb":"say","subject":"message"},"read_memory":{"verb":"read","subject":"memory"},"propose_memory":{"verb":"write","subject":"proposal"},"write_memory":{"verb":"write","subject":"memory"},"close_job":{"verb":"write","subject":"job"},"raise_exception":{"verb":"write","subject":"exception"},"list_exceptions":{"verb":"read","subject":"exception"},"evidence":{"verb":"attach","subject":"evidence"},"cite":{"verb":"attach","subject":"citation"},"rank":{"verb":"decide","subject":"order"},"gate":{"verb":"decide","subject":"gate"}},"provider":{"boundary":{"never":["close an item: only a person closes, with an outcome","take an owner or a due time from the agent that raised the item instead of the routing table","write a second open item for a record and type that already has one","write an item with no record id, or one the system of record does not hold","act on a dispute or a hardship claim, or send anything about one to anyone but its named owner","escalate an item more than once, or before it is due"],"stays_with_person":["closing every item, with the outcome","writing the routing table: who owns each type, the clock, who it escalates to","every dispute and hardship claim","deciding when a label that keeps arriving as other becomes a type"]},"job":"everything the other standards could not decide, in one queue with an owner and a clock","skill":"exception-desk","stub":"local.mock_exception_queue","production":null,"product":null,"insert":null,"with_us":{"kind":"scoped_build","note":"Office Voice's handoff inbox holds the items its own calls raise, each with a typed reason, today. Letting a sheet, a bot or a flow write to that inbox is being built and is not live, so until it is, the one queue for every surface is the business's own sheet, list or table, and joining it to Office Voice is quoted as a scoped build."}},"with_us_next":"Office Voice's handoff inbox holds the items its own calls raise, each with a typed reason, today. Letting a sheet, a bot or a flow write to that inbox is being built and is not live, so until it is, the one queue for every surface is the business's own sheet, list or table, and joining it to Office Voice is quoted as a scoped build. Send the failing check id for a fixed price; do not build the live step yourself.","default_path":null,"boundary":{"never":["close an item: only a person closes, with an outcome","take an owner or a due time from the agent that raised the item instead of the routing table","write a second open item for a record and type that already has one","write an item with no record id, or one the system of record does not hold","act on a dispute or a hardship claim, or send anything about one to anyone but its named owner","escalate an item more than once, or before it is due"],"stays_with_person":["closing every item, with the outcome","writing the routing table: who owns each type, the clock, who it escalates to","every dispute and hardship claim","deciding when a label that keeps arriving as other becomes a type"]},"substrate":{"how_to_read":"These are roles, not products. Work out which you already have by matching `providers[].detect` against the repository, then build or add only the gap. No check asserts against a vendor, so any substrate that answers the role passes the same checks.","requires":[{"role":"record","label":"Somewhere the book lives","need":"Durable storage that the workflow owns the write path to, keyed so the same record can be found twice.","when_missing":"Half a day. Any Postgres will do and the free tiers are ample at this size. The trap is not the database, it is letting people edit around the app that is supposed to own the value.","ports":["read_job","read_ledger","read_rule","read_inbox","raise_exception","list_exceptions"]},{"role":"send","label":"Something that sends","need":"Email or SMS from a domain the recipient recognises, with delivery visible enough to tell a bounce from a silence.","when_missing":"An hour for the wiring, longer for the DNS. Sending from a domain you do not own is the single fastest way into a spam folder.","ports":["notify"]},{"role":"surface","label":"Something a person opens","need":"A view of the book that whoever does this job can read and act on without asking a developer.","when_missing":"Start at tier 0 and stop there until the business asks for more. The cheapest surface is not a screen, it is a scheduled email carrying the list, and for one person doing this job that is not a compromise, it is the right answer. On Microsoft 365 or Workspace you can skip straight past this: a list and a sheet are already what people open. Build upward only when two people need the same list at the same time.","tiers":[{"tier":0,"label":"A scheduled digest carrying the list","cost":"About an hour. Needs nothing you do not already have.","satisfiedBy":"send","whenYouNeedIt":"One person does this job. They want to know what is waiting when they sit down, and they act on it from their inbox."},{"tier":1,"label":"A read-only page showing the book","cost":"An afternoon. One query and one table.","whenYouNeedIt":"Somebody needs to look mid-day without waiting for tomorrow's digest, or to answer a customer on the phone."},{"tier":2,"label":"Editable rows, a queue and a log","cost":"A day or two with a coding agent, which is what agents are best at. It is bounded work: the fields are the schema, the queue is whatever escalate hands off, the log is the trace.","whenYouNeedIt":"More than one person shares the book and they need to correct it without treading on each other, and you need to see afterwards who changed what."}],"already_reachable":[{"tier":0,"label":"A scheduled digest carrying the list","because":"this standard already requires the send role, so nothing new is needed for it"}],"ports":["escalate","publish_report"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"names-its-record","n":1,"title":"Every item names its record by id, read from the system that holds it","statement":"An item points at the job, quote or invoice it is about by the id the system of record issued, and the desk reads that record before it writes the item. A raise with no record id, or with an id the system does not hold, is sent back to the standard that raised it, and nothing is written.","assertable":"both","ports":["read_inbox","read_job","read_ledger","raise_exception","notify"],"stimulus":"Three raises arrive. Quote Out parks job J-1042 in ServiceM8 on a line not in the price book. A bot raises a parts shortfall for \"Mrs Patel's hot water job\" with no record id. Invoice Out parks Xero invoice INV-9999, which the ledger does not hold.","assertion":"Exactly one item exists, for J-1042, naming ServiceM8 and J-1042, and a read of J-1042 came before its write. The other two raises are each sent back to the standard that raised them, naming why. No item exists without a record id.","stubLimit":"On the stub the job system and the ledger are maps the runner filled. Only a live tenant proves the id read is the one ServiceM8, Simpro, AroFlo, Fergus or Xero actually holds, and that a job merged or deleted since is refused."},{"id":"type-from-the-closed-list","n":2,"title":"The type comes from the closed list","statement":"Every item carries one type from the standard's closed list. A raise with any other label is stored as other, with the label it arrived with kept in the reason, and never as a new type. The daily summary lists the labels that arrived as other, so one that keeps recurring becomes a type by a person's decision.","assertable":"stub","ports":["read_inbox","read_job","raise_exception"],"stimulus":"A bot raises an item on a real job with the label missing_permit, which is not on the list, and Quote Out raises price_not_in_book on another.","assertion":"Two items. The first has type other and its reason contains missing_permit. The second has type price_not_in_book. No item in the queue has a type outside the closed list."},{"id":"owner-and-clock-from-routing","n":3,"title":"The owner and the clock come from the routing table, never from the agent that raised it","statement":"The business writes a routing table: for each type, the person who owns it, how many hours they have, and who it goes to if it is late. The desk reads the table before it writes an item and takes the owner and due time from it. An owner or due time the raising agent suggests is ignored.","assertable":"stub","ports":["read_inbox","read_job","read_rule","raise_exception"],"stimulus":"A bot raises a parts shortfall on J-1042 and suggests itself as owner, due in a week. The routing table says parts shortfalls go to Mel with four hours on the clock.","assertion":"The item's owner is Mel and its due time is four hours after it was raised. A read of the routing table came before the write, and the write carries neither the suggested owner nor the suggested due time."},{"id":"one-item-per-record-and-type","n":4,"title":"One item per record and type: a second raise updates the open item","statement":"The desk reads the open items before it writes. A second raise of the same type on the same record updates the open item's reason and raise count and adds no row, and it does not restart the clock. A different type on the same record is a separate item. Once a person has closed an item, a new raise opens a new one.","assertable":"stub","ports":["read_inbox","read_job","list_exceptions","raise_exception"],"stimulus":"A bot raises a parts shortfall on J-1042. Two hours later Parts Ready raises the same shortfall with a new reason, and Job Close raises write_failed on the same job.","assertion":"Two open items on J-1042. The shortfall item keeps its id and first due time, carries the newer reason and a raise count of two. The write_failed item is separate. No two open items share a record and a type."},{"id":"nothing-auto-closes","n":5,"title":"Nothing closes itself: a person closes every item, with an outcome","statement":"The desk has no way to close an item. A raising standard may mark an item resolved upstream (the invoice was paid, the part turned up), and the item stays open with that mark until a person closes it with an outcome. A close is recorded with who closed it, when, and the outcome they wrote.","assertable":"stub","ports":["read_inbox","list_exceptions","raise_exception"],"stimulus":"Two open items. Get Paid reports the invoice behind the first one was paid. The office manager closes the second on the queue's screen with the outcome \"rang supplier, part on the 7:30 run\".","assertion":"The first item is still open and marked resolved upstream. The second is closed by the office manager with her outcome and time. No write the desk attempted carried a closed status or a close."},{"id":"disputes-stay-with-a-person","n":6,"title":"A dispute or a hardship claim is never worked by an agent","statement":"A dispute or hardship item goes to the person the routing table names for it, and carries a stop that every chase standard reads before it acts on that record (Debtor Chasing, Follow-Up, Cash Seam). The desk sends nothing about it to anyone but that person, whatever the raising agent suggested doing next.","assertable":"stub","ports":["read_inbox","read_ledger","read_rule","raise_exception","notify","escalate"],"stimulus":"A Front Desk call raises a dispute on invoice INV-2207, suggesting a payment plan be sent. A bot raises hardship on INV-2210. The routing table names the owner, Dana, for both.","assertion":"Both items are owned by Dana and carry the chase stop, so a chase on either invoice is held. No notify or escalate about either record went to anyone but Dana."},{"id":"overdue-escalates-once","n":7,"title":"An overdue item escalates once","statement":"When an open item passes its due time, the desk escalates it once, to the person the routing table names for that type. It does not escalate early, does not escalate again on the next run, and never escalates a closed item.","assertable":"stub","ports":["read_inbox","read_rule","list_exceptions","escalate"],"stimulus":"A parts shortfall due at 2pm, with the routing table naming Dana as the escalation. The desk runs at 1pm, 3pm, 7pm and the next morning. A second item due at 2pm is closed by a person at 1:30pm.","assertion":"Exactly one escalation for the first item, to Dana, made at or after 2pm. None for the closed item."},{"id":"daily-summary-even-when-empty","n":8,"title":"The daily summary goes out by the set time, including when there is nothing in it","statement":"Once a day, by the time the business set, the desk publishes a summary: open items counted by type and by owner, the overdue items listed, and the labels that arrived as other. When nothing is open it still publishes, and says none.","assertable":"both","ports":["read_inbox","read_rule","list_exceptions","publish_report"],"stimulus":"Two mornings with the summary set for 7am. On the first, three items are open and one is overdue. By the second, a person has closed all three. The desk runs at 6am and 7:05am each day.","assertion":"One summary each day, published at or after 7am and before 8am. The first counts three by type and by owner and lists the overdue one. The second has no items and says none.","stubLimit":"On the stub a summary is a record the runner reads. Only the live run proves it reached the owner's inbox or Teams channel on the morning it says, and that the counts match the queue the person sees."},{"id":"pass-test","n":9,"title":"The pass test passes","statement":"On the business's own systems, three exceptions from three different ways of running land in one queue: Invoice Out's sheet parks an invoice on an unpriced line, a Front Desk call escalates an address it could not resolve, and a bot raises a parts shortfall. Each has its record id, its owner from the routing table and a due time. A second raise of the shortfall leaves one item, one item going overdue escalates once, and the next day's summary lists all three.","assertable":"production","ports":["raise_exception","list_exceptions","read_rule","read_job","escalate","publish_report"],"stimulus":"Real raises from three surfaces against the business's own job system and ledger: the Invoice Out sheet, a Front Desk call on the real line, and an AI assistant on its own key. Then the bot raises the shortfall again, one item is left past its due time, and the next morning arrives.","assertion":"One queue holds three items, each naming a record id the job system or ledger holds, each owned by the person the routing table names, each with a due time, each naming the surface it came from. The repeated shortfall is still one item. One escalation for the overdue item. The next summary lists all three.","stubLimit":"Deliberately unpassable on a stub. On a stub every raise comes from the runner, in one shape, so one queue is trivial. The test is three surfaces built by different people (a sheet, a phone call and a bot) all writing to the same queue with the same record ids, and only the business's own systems prove they do."}]}