{"spec":"AIP-BS-CASH-SEAM","version":"2026-10-03","standard":"cash-seam-build-standard","source":"https://aipathway.com.au/explore-ai/cash-seam-build-standard","markdown":"https://aipathway.com.au/explore-ai/cash-seam-build-standard.md","licence":"CC BY 4.0","how_to_read":"Seven 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 runs with us. 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. Runs on the production provider (see with_us_next)","evidence_required":"not a software check. Attach what proves it"},"tally":{"total":9,"onStub":7,"productionOnly":1,"partial":3,"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":["mint a key, or match a record on a customer name or a phone number, once an upstream id exists","make a second job for an enquiry that already has an open one","treat a write with no external id, or a chat or connector confirmation, as a write","send a quote, an invoice or a chase without a release naming a person or a rule","move a hop only while a chat session is open","proceed past an open exception"],"stays_with_person":["every exception, by the owner named for its stage","the release, or the rule that stands in for it","deciding two enquiries are the same job when the number does not say so","anything the four standards it binds keep with a person"]},"job":"one record survives from the call to the chase","skill":"cash-seam","stub":"local.mock_cash_path","production":"office_voice.recover","product":"Office Voice / Recover","insert":"Get Paid","with_us":{"kind":"enable_insert","provider":"office_voice.recover","product":"Office Voice / Recover","insert":"Get Paid"}},"with_us_next":"Enable Get Paid on Office Voice / Recover; npx @aipathway/conformance connect gets the owner a key and the links to connect their systems. There is no production run of these checks yet: AIPATHWAY_PROVIDER=office_voice.recover needs a live port map that is not published, so the stub run is the finished build. Do not hand-roll the live step.","default_path":"The live step runs on Office Voice / Recover (Get Paid) by default. Build everything else to the standard, then connect the live step with npx @aipathway/conformance connect: the owner approves in a browser and connects their systems there. Do not ask the owner to create a developer app, copy client keys or set up OAuth, and do not write the live step yourself. Build your own version only if the owner has been told about the hosted step and asks for that.","boundary":{"never":["mint a key, or match a record on a customer name or a phone number, once an upstream id exists","make a second job for an enquiry that already has an open one","treat a write with no external id, or a chat or connector confirmation, as a write","send a quote, an invoice or a chase without a release naming a person or a rule","move a hop only while a chat session is open","proceed past an open exception"],"stays_with_person":["every exception, by the owner named for its stage","the release, or the rule that stands in for it","deciding two enquiries are the same job when the number does not say so","anything the four standards it binds keep with a person"]},"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":["write_job","read_job","draft_quote","write_invoice","read_ledger","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":["send_sms","issue_notice"]},{"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":"identity-from-the-system-of-record","n":1,"title":"Every downstream step takes its id from the system of record, never from a name or a phone number","statement":"Once a job exists, every later step reads the upstream record's id from the job system or the ledger: from the event that woke it, or from a read of that record by id. It never mints its own key and never matches on a customer name or a phone number.","assertable":"stub","ports":["read_job","draft_quote","write_invoice"],"stimulus":"Two open jobs for two different customers who share a name. One of them has a quote accepted, and the job is completed.","assertion":"The quote carries its job's id from the job system and the invoice carries that quote's id and that job's id. No search by name or phone runs while a downstream event is handled. An invoice on the other customer's job, or any reference that is not an id the system issued, fails."},{"id":"one-job-per-enquiry","n":2,"title":"One job per enquiry: a repeat call or a second door attaches to the open job","statement":"Before writing a job, the build reads for an open job on that customer or number. A repeat call, or the same customer arriving through a second door, attaches to the open job with duplicate_of set. A second job is a fail.","assertable":"stub","ports":["read_job","write_job"],"stimulus":"The same caller rings twice in one evening: once on the phone line, then again through the web form, from the same number.","assertion":"Exactly one job exists for that number. The second enquiry carries duplicate_of with the first job's id. A read for open jobs on that number preceded every job write. A second job, or a job write with no read before it, fails."},{"id":"no-external-id-no-write","n":3,"title":"A write that comes back without an external id is treated as not written","statement":"A write into the job system or the ledger that returns a null or missing external_id did not happen, as far as the chain is concerned. The build escalates it and nothing downstream acts on it: no quote from that job, no send of that quote, no chase of that invoice.","assertable":"both","ports":["draft_quote","write_invoice","escalate","gate"],"stimulus":"The full chain, with the runner injecting a null external_id on the quote write for one job and on the invoice write for another.","assertion":"Each null write is escalated with reason write_failed. Nothing is released, sent or chased for either job, and no later record carries a null or invented reference in its place. Treating the null as a success fails.","stubLimit":"The stub returns a null id when the runner tells it to. Only a real job system and a real ledger show the ways a real write comes back without one: a timeout after the record was made, an empty body on a success code, a rate limit mid-batch."},{"id":"the-chain-carries-the-id","n":4,"title":"The quote carries the job id, the invoice carries the quote or job id, and the chase reads the invoice id off the live ledger","statement":"Each record carries the id of the one before it. The quote names the job. The invoice names the accepted quote and the job. The chase names an invoice id the ledger returned when the build read it. A break anywhere in the chain is a fail.","assertable":"stub","ports":["read_job","draft_quote","write_invoice","read_ledger","send_sms"],"stimulus":"One enquiry through the whole path: the job is created, the quote is accepted, the job is completed and the invoice goes overdue.","assertion":"Exactly one job, one quote, one invoice and one chase exist. The quote's job id is the job's id, the invoice's quote id and job id are those records' ids, and the chase's invoice id appeared in a ledger read made before it. Any missing or mismatched link fails."},{"id":"confirmation-is-not-a-write","n":5,"title":"A chat or connector confirmation is not a write and not a release","statement":"A write counts only when it is read back through the port from the system of record. Every send to the customer, the quote, the invoice and the chase, waits for a release that names a person or a named rule. The model saying it is done is neither.","assertable":"both","ports":["read_job","read_ledger","gate","issue_notice","send_sms","escalate"],"stimulus":"The full chain, with the runner making one quote write return an id and a success message while storing nothing, as a connector that answered before the record existed would.","assertion":"That quote is escalated and never released or sent. Every send was preceded by a read through the port that returned the record, and by a release naming a person or a rule the business set. A send with no read-back, or a release with neither a person nor a rule, fails.","stubLimit":"The stub proves the read-back and the release are on the trace. Only a real tenant proves the record read back is the record the customer will see, and that the release came from the person or rule it names."},{"id":"named-unattended-trigger","n":6,"title":"Every hop starts from a named event, and none waits for a chat session","statement":"Each hop is woken by a named event arriving by webhook, schedule or polling: job created, quote accepted, job completed, invoice overdue. A build that only moves while somebody has a chat window open fails, and so does one that acts on a chat message as if it were an event.","assertable":"both","ports":["write_job","draft_quote","write_invoice","send_sms"],"stimulus":"The full chain delivered as events with nobody in a session, plus one chat message asking for an invoice to be raised.","assertion":"Every write and every chase names the event that woke it, the event is one the runner delivered by webhook, schedule or polling, and each hop's event is the right kind for that hop. All four hops happen. A hop missing, or a write woken by the chat message, fails.","stubLimit":"The stub delivers the events itself. Only production proves the webhook is registered, the schedule actually fires overnight, and the poll survives a restart with nobody watching."},{"id":"exception-owner-travels","n":7,"title":"An exception names its owner, travels with the record, and the next step does not pass it","statement":"A parked or escalated item names the person who owns it, taken from the owners the business set for that stage. The exception stays on the record, and the next step reads it and stops rather than proceeding past it.","assertable":"stub","ports":["read_rule","read_job","escalate","write_invoice"],"stimulus":"Two jobs complete. One already carries an open exception a person set on it. The invoice write for the other comes back with no external id.","assertion":"No invoice is raised for the job carrying the exception, and a hold names that exception's owner. The failed write is escalated naming the owner the business set for invoices. An escalation with no owner, an owner the business never named, or an invoice raised past the exception, fails."},{"id":"survives-the-builder","n":8,"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 each hop's trigger and the writes into the job system and the ledger 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":9,"title":"The pass test passes","statement":"On the business's own job system and ledger: the same caller rings twice, the quote is accepted, the job completes and the invoice goes overdue. Exactly one job, one quote, one invoice and one chase exist, each carrying the upstream id. A null external id injected anywhere stops the chain.","assertable":"production","ports":["read_job","write_job","draft_quote","write_invoice","read_ledger","send_sms"],"stimulus":"Real events on the real tenant: two real calls from one number, a real acceptance, a real completion, a real due date passing, then the same run again with one write forced to return no id.","assertion":"Each record in the job system and the ledger holds the id of the one before it, as issued by those systems. The second call attached to the first job. The forced null produced an escalation with an owner and nothing downstream of it.","stubLimit":"Deliberately unpassable on a stub. A stub issues whatever ids it likes and links whatever it is told to, so a build can agree with itself there and still key the real invoice to a name. Only the business's own systems prove the id that left one hop is the id that arrived at the next."}]}