{"spec":"AIP-BS-DISPATCH-BOARD","version":"2026-10-03","standard":"dispatch-board-build-standard","source":"https://aipathway.com.au/explore-ai/dispatch-board-build-standard","markdown":"https://aipathway.com.au/explore-ai/dispatch-board-build-standard.md","licence":"CC BY 4.0","how_to_read":"Ten of these twelve 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. One is not a software check at all: it asks what you measured before you started.","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":12,"onStub":10,"productionOnly":1,"partial":2,"preconditions":1},"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"},"evidence":{"verb":"attach","subject":"evidence"},"cite":{"verb":"attach","subject":"citation"},"rank":{"verb":"decide","subject":"order"},"gate":{"verb":"decide","subject":"gate"}},"provider":{"boundary":{"never":["slot a job whose address has not resolved","assign against a licence rule","drop a displaced job","message a customer without consent","message twice for one change"],"stays_with_person":["accepting an emergency displacement","deciding a job is outside the area","anything said to an upset customer","keeping each tech's licence record true, codes and expiry"]},"job":"the right person at the right house at the right time","skill":"dispatch-board","stub":"local.mock_board","production":null,"product":null,"insert":null,"with_us":{"kind":"scoped_build","note":"No hosted dispatch step. Office Voice already writes the booked time into ServiceM8 and Simpro, one slot per job with the id back, but only inside a Front Desk (Booked After Hours) call; a board cannot call it and it reads no licences. For a board, the write into the job system's schedule and the customer message with consent are quoted as a scoped build."}},"with_us_next":"No hosted dispatch step. Office Voice already writes the booked time into ServiceM8 and Simpro, one slot per job with the id back, but only inside a Front Desk (Booked After Hours) call; a board cannot call it and it reads no licences. For a board, the write into the job system's schedule and the customer message with consent are 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":["slot a job whose address has not resolved","assign against a licence rule","drop a displaced job","message a customer without consent","message twice for one change"],"stays_with_person":["accepting an emergency displacement","deciding a job is outside the area","anything said to an upset customer","keeping each tech's licence record true, codes and expiry"]},"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":["resolve_address","write_job","read_job","evidence","read_rule"]},{"role":"schedule","label":"Something that runs it on time","need":"A trigger that fires on a cadence and can call an authenticated endpoint.","when_missing":"An hour. Expose one authenticated endpoint that starts the work and let anything call it, so the schedule is never welded to whoever is hosting you.","ports":["gate"]},{"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","send_sms"]},{"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"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"address-verified-before-slot","n":1,"title":"No slot until the address resolves","statement":"A job is given a slot only after its address resolves to a real place. An address that will not resolve leaves the job unslotted and named to a person, never slotted to where somebody hopes it is.","assertable":"stub","ports":["resolve_address","write_job"],"stimulus":"Two jobs in the system: one with an address that resolves, one with an address that does not.","assertion":"The resolve call precedes the slot write for the first job and the slot carries the resolved address. No slot exists for the second job. A slot on an unresolved address fails."},{"id":"licence-is-a-rule-not-a-vibe","n":2,"title":"A licence requirement is a rule read from the record, never assumed","statement":"A job carrying a licence requirement (gas, electrical, height) is assigned only to a tech whose licence record covers that code and is current on the day. The rule is read before the assignment. No person overrides it, and no build infers it from a name or a job history.","assertable":"stub","ports":["read_rule","write_job"],"stimulus":"A gas job and three techs: one holding a current gas licence, one whose gas licence has expired, one with no licence at all.","assertion":"The rule for the code is read before the slot is written and the assigned tech is the one holding the current licence. An assignment to the expired or unlicensed tech fails."},{"id":"window-and-travel-are-data","n":3,"title":"The slot sits inside the customer's window and after the previous job plus travel","statement":"A slot starts no earlier than the customer's window opens and ends no later than it closes. Where the tech has a previous job, the slot starts after that job's end plus the travel from its resolved address. Travel is read, never guessed.","assertable":"both","ports":["read_job","write_job"],"stimulus":"Two jobs on one tech with morning windows and a travel table between their resolved addresses.","assertion":"Each slot is inside its job's window. The second slot starts at or after the first slot's end plus the travel read between the two addresses. A slot that overlaps, or starts before the travel allows, fails.","stubLimit":"The stub's travel is a table the runner seeded, so the shape is proved here. Only a live map proves the minutes between two real addresses on the day."},{"id":"one-slot-per-job","n":4,"title":"One slot per job, written once, updated on a second event","statement":"The write is keyed to the job. A second scheduling event for the same job updates the existing slot or does nothing. A null external id is a failed write, not a slot.","assertable":"stub","ports":["read_job","write_job"],"stimulus":"The same job's scheduling event delivered twice, and one write that returns no id.","assertion":"Exactly one slot exists for the job, with an external id, and the second event produced an update or no write. The write that returned no id was escalated and nothing was sent about it. A second slot, or a null id treated as success, fails."},{"n":5,"assertable":"both","stubLimit":"The stub proves nothing leaves without a release event carrying a named person. Only the live path proves what left is what was released, rather than something regenerated after the approval.","id":"emergency-displaces-with-a-person","title":"An emergency takes a slot only when a named dispatcher accepts the displacement","statement":"An emergency may displace a slotted job only after a named person accepts that displacement. The displaced job is re-slotted or escalated with a reason and an owner. It is never silently dropped from the board.","ports":["gate","escalate","write_job"],"stimulus":"A full board with one tech, then an emergency arriving whose window only that tech's slotted time can meet.","assertion":"A gate call naming the dispatcher, with an acceptance, precedes the emergency's slot write. The displaced job still has exactly one slot, marked with what displaced it, and it either sits outside the emergency's time or has an escalation naming an owner. An emergency slot with no acceptance, or a displaced job with no slot and no escalation, fails."},{"id":"customer-told-once-with-consent","n":6,"title":"The customer is told once per slot, on a channel they consented to","statement":"One confirmation goes to the customer for each slot, on a channel with consent recorded, after the consent is checked. A second identical message, or a message to a customer with no consent on that channel, is a failure. A customer without consent is named to a person to phone.","assertable":"stub","ports":["send_sms","gate"],"stimulus":"Two jobs slotted: one customer with consent recorded for the channel, one with none. The scheduling event is then delivered a second time with nothing changed.","assertion":"The consenting customer has exactly one message, preceded by a consent check, and none after the second event. The other customer has no message and an escalation names them. A second message, or one sent without consent, fails."},{"id":"tech-told-with-the-record","n":7,"title":"The tech's notification carries the job, the address, the window and what the customer said","statement":"The tech is told with the record: the job id, the resolved address, the customer's window, and the customer's own words about the fault. A bare notification saying there is a new job is not a notification.","assertable":"stub","ports":["notify"],"stimulus":"One job slotted whose customer described the fault when booking.","assertion":"The notification to the assigned tech carries the job id, the resolved address, the window and the customer's words. A notification missing any of those fails."},{"id":"change-propagates-once","n":8,"title":"A change to the job moves the slot and tells each affected party exactly once","statement":"When the job's time, address or tech changes after the slot is written, the slot is updated in place and one fresh message goes to each party the change touches. A change that leaves the old slot in place, or that messages anybody twice, is a failure.","assertable":"stub","ports":["read_job","write_job","send_sms","notify"],"stimulus":"A slotted job whose customer window is moved by the runner after the slot is written and the customer told.","assertion":"After the change, the job still has exactly one slot and it sits inside the new window. Exactly one new message went to the customer and exactly one to the tech. A stale slot, a second slot, or a doubled message fails."},{"id":"unassignable-is-a-queue","n":9,"title":"A job that cannot be slotted is escalated with a reason and an owner","statement":"No licensed tech, an address that will not resolve, a window no tech can meet: each leaves the job in a queue with a reason and a named owner. Nothing is left off the board in silence.","assertable":"stub","ports":["escalate"],"stimulus":"Three jobs that cannot be slotted: one needing a licence no current tech holds, one whose address does not resolve, one whose window is shorter than the job.","assertion":"None of the three has a slot. Each has an escalation carrying the job id, a reason from the list and an owner. A job with no slot and no escalation fails."},{"id":"the-record-by-job","n":10,"title":"Who was assigned, when, by whom or by which rule, and what the customer was told are retrievable by job","statement":"When the customer says nobody told them, the record shows the slot, the tech, the person or rule that set it, and the message the customer was sent with its time.","assertable":"stub","ports":["read_job","evidence"],"stimulus":"One job slotted, the customer confirmed and the tech told.","assertion":"A read by job id returns the slot with its tech, start, external id and the person or rule that wrote it, and the confirmation attached as evidence with its sent time. A record missing the author or the message fails."},{"id":"survives-the-builder","n":11,"title":"It survives the builder","statement":"Someone other than the builder can explain what it does, and it runs on an account the business owns.","assertable":"precondition","ports":["read_rule"],"stimulus":"No stimulus. The builder names who else can explain it and whose account the board and the licence rules runs on.","assertion":"The runner reports what was declared and never scores it green on its own say-so.","stubLimit":"Not a runtime property. Software cannot prove a second person understands it, and outliving its author is precisely what is being asked about."},{"id":"pass-test","n":12,"title":"The pass test passes","statement":"On the real job system: one job scheduled twice, one licence-gated job, one emergency displacing a slot with a dispatcher's acceptance, one change to a slotted job.","assertable":"production","ports":["read_job","resolve_address","write_job","send_sms"],"stimulus":"Real scheduling events on the business's own tenant, driven by creating and changing real jobs.","assertion":"One slot per job in the job system with its own id, the licence-gated job on the licensed tech only, the emergency slot carrying the dispatcher's acceptance and the displaced job re-slotted, the changed job moved once with one fresh message each, and every customer message on a consented channel.","stubLimit":"Deliberately unpassable on a stub. A stub accepts every write, resolves every address you seed and holds whatever licence you give it. Only the real tenant proves the id is real, the licence record is the one the business keeps, and the second event did not make a second slot."}]}