{"spec":"AIP-BS-MONTH-END-CLOSE","version":"2026-11-12","standard":"month-end-close-build-standard","source":"https://aipathway.com.au/explore-ai/month-end-close-build-standard","markdown":"https://aipathway.com.au/explore-ai/month-end-close-build-standard.md","licence":"CC BY 4.0","how_to_read":"Eleven of these thirteen 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 against your own live systems. 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 against your own live systems (see with_us_next)","evidence_required":"not a software check. Attach what proves it"},"tally":{"total":13,"onStub":11,"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"},"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"},"read_acceptance":{"verb":"read","subject":"acceptance"},"read_contract":{"verb":"read","subject":"contract"},"read_assessment":{"verb":"read","subject":"assessment"},"draft_claim":{"verb":"write","subject":"claim_draft"},"read_menu":{"verb":"read","subject":"menu"},"request_payment":{"verb":"write","subject":"payment_request"},"read_payment":{"verb":"read","subject":"payment"},"write_booking":{"verb":"write","subject":"booking"},"read_signoff":{"verb":"read","subject":"signoff"},"propose_journal":{"verb":"write","subject":"journal_proposal"},"set_lock_date":{"verb":"write","subject":"lock_date"},"evidence":{"verb":"attach","subject":"evidence"},"cite":{"verb":"attach","subject":"citation"},"rank":{"verb":"decide","subject":"order"},"gate":{"verb":"decide","subject":"gate"}},"provider":{"boundary":{"never":["post a journal no person approved in a recorded sign-off","post an approved journal twice, or retry a post without reading back first","invent an accrual or prepayment amount; it comes from the schedule the business keeps or a person's entry for the period","set the lock date before sign-off, or to a date earlier than the one the ledger holds","mark an item passed without the ledger read it came from","decide whether a difference is material, or recode an uncoded line"],"stays_with_person":["reviewing the close pack and signing off, by name, approving each proposed journal","keeping the schedule of accruals and prepayments, and entering any amount the schedule does not hold","whether any difference or failed item matters, and what to do about it","setting the lock date by hand where the ledger's API cannot write it","what each uncoded line was, through the Uncoded Query standard"]},"job":"the month closed the same way every time, signed off, then locked","skill":"month-end-close","stub":"local.mock_ledger_close","production":null,"product":null,"insert":null,"with_us":{"kind":"own_systems","note":"No hosted step. The build connects straight to the business's own ledger through the ledger's own sign-in, and the production check runs there: the approved journals and the lock date written and read back after a person's sign-off."}},"with_us_next":"No hosted step. The build connects straight to the business's own ledger through the ledger's own sign-in, and the production check runs there: the approved journals and the lock date written and read back after a person's sign-off. There is nothing to buy: your build does this step on the business's own systems. No production run is offered yet, because the live port map is not published, so do not run AIPATHWAY_ENV=production. Run the pass test by hand on the real systems and attach the evidence.","default_path":null,"boundary":{"never":["post a journal no person approved in a recorded sign-off","post an approved journal twice, or retry a post without reading back first","invent an accrual or prepayment amount; it comes from the schedule the business keeps or a person's entry for the period","set the lock date before sign-off, or to a date earlier than the one the ledger holds","mark an item passed without the ledger read it came from","decide whether a difference is material, or recode an uncoded line"],"stays_with_person":["reviewing the close pack and signing off, by name, approving each proposed journal","keeping the schedule of accruals and prepayments, and entering any amount the schedule does not hold","whether any difference or failed item matters, and what to do about it","setting the lock date by hand where the ledger's API cannot write it","what each uncoded line was, through the Uncoded Query standard"]},"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_ledger","evidence","read_rule","write_ledger","raise_task","raise_exception","read_signoff","propose_journal","set_lock_date"]},{"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"]}],"registry":"https://aipathway.com.au/substrates.json"},"checks":[{"id":"every-item-has-a-result","n":1,"title":"Every item on the checklist gets a result","statement":"For every entity and every period, each of the eight items (bank, uncoded, receivables, payables, payroll, accruals, tax control, clearing) is recorded as pass, fail or could not check. None is skipped, and a blank is never read as a pass.","assertable":"stub","ports":["read_rule","read_signoff","evidence"],"stimulus":"Three entities closing October, run on an ordinary day.","assertion":"Every entity has a recorded result for all eight items, each one pass, fail or could_not_check."},{"id":"evidence-or-it-did-not-pass","n":2,"title":"A pass or a fail carries its evidence","statement":"Every pass and every fail names the ledger read it came from, with the time of the read and what it showed: the balances compared, the lines found, the dates missing. A pass with no read behind it is not a pass.","assertable":"stub","ports":["read_ledger","read_rule","evidence"],"stimulus":"Three entities closing October.","assertion":"Every pass and fail result names a read the world served, for the same entity and the read that item needs, made before the result was recorded."},{"id":"could-not-check-said-plainly","n":3,"title":"What could not be read is recorded as could not check","statement":"When the ledger does not answer, or a figure the item needs is not available, the item is recorded as could not check with the reason. It is never a pass because nothing was found, and never a fail the build made up.","assertable":"stub","ports":["read_ledger","evidence"],"stimulus":"The ledger does not answer the bank read or the payroll read for one entity.","assertion":"Both items for that entity are recorded as could_not_check with a reason, and neither is recorded as pass or fail."},{"id":"bank-to-the-statement-date","n":4,"title":"Every bank account is reconciled to a statement dated at period end","statement":"Each bank and card account passes only when its last reconciled statement line is dated on or after the period end, the ledger balance equals the statement balance at that date, and no statement lines are left unreconciled. One account short fails the item, and the result names the account.","assertable":"both","ports":["read_ledger","evidence"],"stimulus":"One entity with a card account reconciled only to the 27th; another with every account reconciled to the 31st and balances that agree.","assertion":"The first entity's bank item is a fail naming the card account; the second's is a pass.","stubLimit":"The stub hands the build the statement date and balance as fields. Live, each ledger exposes reconciliation differently, and some do not expose the statement balance to an app at all; only the business's own ledger proves the build reads the right fields, or records could not check where it cannot."},{"id":"uncoded-handed-on","n":5,"title":"Uncoded and suspense lines are handed to Uncoded Query, once","statement":"Every line left uncoded or sitting in a suspense account fails the item and is raised once, with its line id, to the Uncoded Query queue, which asks the person who knows what it was. The close never recodes a line itself, and a second run does not raise it again.","assertable":"stub","ports":["read_ledger","raise_exception","evidence"],"stimulus":"One entity with two lines in suspense, and the build run three times.","assertion":"Each line has exactly one exception of kind uncoded, owned by the Uncoded Query queue; that entity's uncoded item is a fail and an entity with none passes."},{"id":"accruals-only-from-the-schedule","n":6,"title":"An accrual amount comes from the schedule or a person, never a guess","statement":"Each accrual and prepayment is proposed as a journal for a person to approve, with its amount taken from the schedule the business keeps or from a person's entry for the period, carrying who entered it. A line with no amount is not proposed: it is raised once to the bookkeeper as no_amount. Last month's figure, an average or a model's estimate is never an amount.","assertable":"stub","ports":["read_rule","propose_journal","raise_exception"],"stimulus":"A schedule with one fixed accrual, one prepayment with no amount and last month's figure beside it, and one accrual a person enters between runs.","assertion":"Every proposal's amount equals the schedule's amount or the person's entry, with the source and who entered it; the line with no amount has no proposal and one no_amount exception."},{"id":"differences-reported-not-judged","n":7,"title":"A difference is reported, never judged small enough","statement":"The tax control account agrees with the return figure for the period, each clearing account is at zero, and the aged receivables and payables equal their control accounts, to the cent. Any difference fails the item with both figures and the difference. Whether a difference matters is the reviewer's call, not the build's.","assertable":"stub","ports":["read_ledger","evidence"],"stimulus":"A clearing account forty cents off zero, and aged receivables ten dollars away from their control account.","assertion":"Both items fail with the figures, and the items with no difference pass."},{"id":"nothing-written-before-sign-off","n":8,"title":"Nothing is written into the ledger before a person signs off","statement":"No journal is posted and no lock date is set or asked for until a named person has signed off that entity's close for that period. Only the journals the sign-off approves are posted. An entity nobody signed off has nothing written.","assertable":"stub","ports":["read_signoff","write_ledger","set_lock_date","raise_task"],"stimulus":"The build runs twice with no sign-off. Then the reviewer signs off one entity, approving one of its two proposed journals, and the build runs again. The other entities are never signed off.","assertion":"Every post, lock date write and lock task comes after a sign-off for its entity; only the approved journal is posted; the entities with no sign-off have no post, lock write or lock task."},{"id":"journals-post-once","n":9,"title":"Each approved journal posts exactly once, and is read back","statement":"Each approved journal is posted with the intent id close:<tenant>:<entity>:<period>:<schedule line>. A retry carries the same id and lands once. A post with no answer is treated as not posted, raised as write_failed to the bookkeeper and tried again. The journals are read back from the ledger before anything else happens.","assertable":"both","ports":["write_ledger","read_ledger","raise_exception"],"stimulus":"Two approved journals; the first post lands but its answer is lost, and the build runs again.","assertion":"The ledger holds exactly one journal per approved proposal, each attempt carried the same intent id, the lost answer was raised as write_failed, and the journals were read back after the last post.","stubLimit":"On the stub one journal per intent id is the runner's memory. Only a real ledger proves a retry after a timeout does not post the accrual twice, through whatever that ledger offers: an idempotency key, or a read back of the narration before a retry."},{"id":"lock-date-forward-after-sign-off","n":10,"title":"The lock date moves forward to period end, only after sign-off","statement":"Once the approved journals read back, the lock date is set to the period end and read back. It is never set before sign-off and never moved earlier than it already is. Where the ledger's API cannot write the lock date, the build does not try: it raises one task for the reviewer to set it by hand, and reads it back.","assertable":"both","ports":["read_ledger","set_lock_date","raise_task"],"stimulus":"Three signed-off entities: one whose ledger cannot write the lock date, one locked to the end of September, and one already locked past the period end.","assertion":"The second is set to 31 October after its journals read back; the first gets one lock task and no write; the third gets neither; no write ever names a date earlier than the lock date it found.","stubLimit":"The stub says which entities can write a lock date. Live, as at 12 November 2026, neither the Xero nor the MYOB Business API writes one and Intuit's reference does not confirm it for QuickBooks Online; only the business's own ledger proves which path it is on and that the date reads back."},{"id":"closed-period-handed-on","n":11,"title":"The closed period is handed to the Recurring Report, once","statement":"Only when the lock date reads back at or after the period end is the period handed to the Recurring Report standard for the client pack, once per entity and period, with the lock date it read.","assertable":"stub","ports":["read_ledger","notify"],"stimulus":"Three signed-off entities; the reviewer sets one entity's lock date by hand a day later.","assertion":"Each entity is handed on exactly once, after a lock read at or after the period end, and the hand-made one only after the person set it."},{"id":"survives-the-builder","n":12,"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 close and its pack 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":13,"title":"The pass test passes","statement":"In the business's own ledger: run the close for one entity with one accrual on the schedule. With no sign-off, nothing is posted and the lock date does not move. After a recorded sign-off, the accrual posts exactly once and reads back, a second run posts nothing, and the lock date reads back at the period end, set by the build where the ledger allows it or by the reviewer on the task it raised.","assertable":"production","ports":["read_signoff","write_ledger","read_ledger","set_lock_date","raise_task"],"stimulus":"One real entity, one scheduled accrual, the build run before and after a person's sign-off, then run again.","assertion":"Before sign-off the ledger shows no new journal and the old lock date; after it, one journal with the intent id in its narration, and the lock date at the period end; the second run adds nothing.","stubLimit":"Deliberately unpassable on a stub. On a stub the journal and the lock date are rows in the runner's memory with ids it made up; live, they have to appear in a ledger the business owns, through its own authentication, after a person's sign-off, and a retry after a timeout is where the second journal comes from."}]}