MCP Integration

Drive Biloh from any AI assistant

Biloh exposes a Model Context Protocol server. Connect Claude, GPT, Grok, or any MCP client and run your business through chat.

Endpoint

https://app.biloh.com.au/api/mcp

Transport

Streamable HTTP / SSE

Tools

469 operator tools

Connect from your client

Paste the configuration below into your MCP client. ReplaceYOUR_PAT_HEREwith a Biloh Personal Access Token issued by your tenant administrator.

Claude Desktop / Claude Code / any JSON-config MCP client

{
  "mcpServers": {
    "biloh": {
      "url": "https://app.biloh.com.au/api/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_PAT_HERE"
      }
    }
  }
}

ChatGPT, Perplexity, Grok (MCP-compatible builds)

Add a custom MCP server in your client's settings with the endpoint URL above and the Bearer token in theAuthorizationheader. The shape matches Claude Desktop's JSON config.

Test from your terminal

curl -X POST https://app.biloh.com.au/api/mcp \
  -H "Authorization: Bearer YOUR_PAT_HERE" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Tool catalogue

Auto-generated from the live MCP registry. Operator-persona tools shown; architect and platform-internal tools are not listed. 469 tools across 11 categories.

Clients & Sites (37)
  • list_clients

    Returns clients for the authenticated user's tenant. Supports filtering by status, free-text search via `query` (case-insensitive substring match across legal_name, trading_name, and billing_email), and pagination via limit/offset. Excludes soft-deleted rows. Excludes test rows (is_test=true) by default — pass include_test:true to include them.

  • get_client

    Returns a single client by ID for the authenticated user's tenant. Returns the full client row including legal_name, trading_name, ABN, billing_email, phone, status, and metadata.

  • get_client_footprint

    Returns a one-shot, read-only dependency rollup for a client (or contractor/site): counts + sample ids of every attached site, contract_service_line (active vs inactive), job (by status), proposal (by status), invoice (by status), and payable, plus the parent's is_test/deleted_at and a test/production child split (`test_rollup`). Counts include test and production children (footprint is a cleanup/offboarding aid, not a dashboard); `test_rollup.is_test_false_count` surfaces live production children hanging off an archived parent. `ids` are capped at 10 per type; `count` is the true total.

  • get_client_portal_link

    Returns the client's persistent portal URL (the /portal/client/{token} link) and its underlying portal token. Mirror of get_contractor_portal_link (WP-CLIENT-PORTAL-LINK-RECOVERY, bugs e843138b + 2178dc8f). The token is always one the portal guard ACCEPTS: it is minted when the column is null, ROTATED when the stored token has aged past the tenant's portal.link_ttl_days, and reused unchanged otherwise. With the TTL at its default of 0 the stored token is returned untouched, so link stability is unaffected. `rotated: true` on the response means the previous link was stale and has been replaced — any copy of that older URL stops working, which is the behaviour a tenant opts into by setting a TTL. The URL host is the tenant's canonical domain.

  • resend_client_portal_link

    Email a client's persistent portal link (the /portal/client/{token} URL) to their stored billing email. The delivery half of lost-email portal recovery — the MCP-native mirror of the operator UI 'Resend portal link' action (POST /api/clients/[id]/resend-portal-link). Returns a link the portal guard accepts: reuses the client's existing portal_token, minting one if null and ROTATING one that has aged past the tenant's portal.link_ttl_days (idempotent, like get_client_portal_link). Open-relay guarded: only ever sends to the client's own stored billing_email, never a caller-supplied recipient (same guard pattern as send_operator_report_email). Returns {client_id, sent, provider_message_id, portal_url}.

  • list_sites

    Returns sites for the authenticated user's tenant. Optionally filtered by client_id. Excludes soft-deleted rows.

  • get_site

    Returns a single site by ID for the authenticated user's tenant. Returns address, geo coords, notes (access / hazards / legal), associated client_id, and metadata.

  • set_site_access_notes_by_service

    Sets (or clears) the per-service access-notes section for a site, keyed by service_id.

  • list_site_contacts

    Returns contacts for a site, for a client, for several clients at once, or every contact across the tenant if no selector is given. Canonical table is `site_contacts` — the legacy `client_contacts` table is deprecated. Each contact has name, role, email, phone, and its notification flags.

  • create_client

    Creates a new client record for the tenant. A client is a business entity that contracts the tenant for services. When billing_email is provided, a default site and billing contact are auto-created so the client's contact details flow onto invoices automatically.

  • update_client

    Updates an existing client's business details, billing info, payment terms, or status. Only provided fields are changed.

  • create_site

    Creates a new site (service location) linked to a client. Sites represent physical locations where services are delivered. Use `access_notes` for contractor access instructions and `legal_notes` for compliance/legal conditions specific to this site.

  • create_site_contact

    Creates a contact person at a site. Contacts receive notifications based on their `contact_role` (owner, primary, billing, backup, emergency, technical). Role defaults auto-set notification flags; explicit flag values override defaults.

  • update_site_contact

    Updates a site contact's details or notification preferences. Only provided fields are changed.

  • archive_site_contact

    Soft-deletes a site contact: sets `deleted_at` so the row stops resolving as a recipient and disappears from list_site_contacts, while the record itself survives for audit. Nothing is hard-deleted.

  • restore_site_contact

    Undoes archive_site_contact: clears `deleted_at` so the contact reappears in list_site_contacts and resolves as a recipient again. The counterpart to archive_site_contact, and the reason archiving is safe to use.

  • update_site

    Updates an existing site's name, address, or notes. Only provided fields change; omitted fields are preserved.

  • onboard_client

    One-tool client onboarding: creates a client, sites, billing contacts, and recurring service lines in one call. Two-phase: confirm=false PREVIEWS (zero writes) with per-line schedule echo, weekday validation, fact echo, and duplicate-client detection; confirm=true creates everything. Sites MUST have a real address (this composite never creates 'Address pending' sites). Lines reference sites by index (site_ref=0 for the first site). Optional align_with_line_ref phase-locks one line's anchor to another's (e.g. inside cleaning on the same day as outside).

  • create_client_compliance_obligation_type

    Add an obligation TYPE to this tenant's CLIENT BUILDING compliance catalogue — the kinds of statutory/periodic obligation a client's property can carry (roof anchor point certification, fire safety systems and the annual occupier's statement, emergency & exit lighting, RCD and test-and-tag, backflow prevention testing, lift registration, pool safety). NOT contractor compliance: that is a separate domain (generate_compliance_report, list_contractor_compliance_documents) which gates whether a contractor may be dispatched. This catalogue is per-tenant DATA, not a fixed platform list — a tenant in another market defines its own obligation types with its own governing standards, by configuration and with no code change. USE WHEN: setting up the building compliance register for the first time, or adding an obligation kind a client turns out to carry. Returns the created type.

  • list_client_compliance_obligation_types

    List this tenant's CLIENT BUILDING compliance obligation catalogue — the kinds of statutory obligation a client's property can carry, with their governing standards and default cadences. NOT contractor compliance. USE WHEN: you need an obligation_type_id before adding an entry to a client's register, or you want to see what this tenant tracks. Returns every active type by default; pass include_inactive to see retired ones too.

  • create_client_compliance_register_entry

    Record that a client (optionally one of its sites) carries a BUILDING compliance obligation — e.g. this body corporate's roof anchor points need certifying to AS/NZS 1891.4. Captures who is responsible, when it was last certified and when it next falls due. If you give last_completed_at and a frequency (or the obligation type carries a default one) the next due date is worked out for you on calendar months, so an annual obligation lands on the anniversary; an explicit next_due_at always wins over that. NOT contractor compliance. PHASE 1 SCOPE: the register only — attaching the certificate document, the client-facing compliance report and its portal download are later phases and are not available yet, so do not promise a client a downloadable report on the strength of this tool. Omit site_id for an obligation that sits with the client as a whole.

  • update_client_compliance_register_entry

    Update one BUILDING compliance obligation on a client's register — most often to record that it has just been certified. Setting last_completed_at RECOMPUTES the next due date from the cadence in force (calendar months, so an annual obligation moves to the anniversary), unless you pass next_due_at explicitly, which always wins. Also used to appoint or change the responsible contractor, change the cadence, or edit the notes. Only the fields you pass are changed. NOT contractor compliance. Returns the entry's new next due date and lock version.

  • get_client_compliance_register

    Read a client's BUILDING compliance register: every statutory/periodic obligation their property carries, who is responsible, when each was last certified, when it next falls due, and its status — current, due soon, overdue, or never certified. Also returns summary counts of the four. Status is DERIVED at read time from the dates and the tenant's own timezone, never stored, so it is correct the moment you ask; the date it was derived against comes back as `as_at`. An obligation that has never been certified reads 'never_certified' rather than 'overdue' — it did not lapse, it was never done. The 'due soon' window is the tenant setting compliance.client_register.due_soon_days (default 30) and is echoed back as `due_soon_days`. NOT contractor compliance — for a contractor insurer pack use generate_compliance_report. PHASE 1 SCOPE: this read and the operator screen only; certificate documents, the client-facing compliance report PDF and the client-portal download are later phases and do not exist yet, so this cannot yet back a written promise of a downloadable report to a client.

  • onboard_client_for_service

    One-step composite to set up a client's service at a site with correct pricing. Creates a contract_service_line and optionally appends special conditions to site.legal_notes. ENFORCES correct data placement: pricing flows to contract_service_line, site conditions flow to site.legal_notes, the service catalog is never touched.

  • get_client_money_story

    Client payment-story composite: fuzzy-resolves a client, sweeps bank_transactions credits for payments that look like theirs (search terms derived from legal/trading name, site-contact names, and any payer_aliases you supply), classifies the payment cadence (weekly | fortnightly | monthly | bulk | irregular), and returns open invoices side-by-side. Amounts are integer cents.

  • create_client_credit

    Records a client credit ON ACCOUNT — money the client is owed that biloh itself did not cause. Every other way a credit comes into existence is a side-effect of something that happened inside biloh (a payment overflowing its invoice, a credit note overflowing, a Stripe prepayment), so before this tool a credit that arrived BEFORE biloh — a pre-cutover overpayment, a deposit carried over from a previous system — could not be represented at all. Amount in INTEGER CENTS. Carries the real-world origin_date (Principle 3) and a required reason, which together are the audit trail. Emits client_credit.created.

  • void_client_credit

    Soft-voids a client credit, reversing ONLY its unconsumed remainder and removing that from the client's credit-on-account balance. A fully-consumed credit is refused (issue a credit note / adjustment instead — applied discounts are never clawed back); a partially-consumed credit voids the remaining balance and reports the consumed vs voided split. Emits client_credit.voided.

  • get_client_statement

    Returns the aggregated client statement view for a date range: opening balance, in-period invoices/payments/refunds, closing balance, outstanding amount, and per-PO subtotals. Amounts in integer cents. Pure read — does NOT persist a statement row (use propose_send_statement for that).

  • get_client_audit_timeline

    Returns the chronological financial event timeline for one client over a period. Reads `domain_events` filtered to invoice/payment/refund/statement/payable/proposal/client entities owned by the client. Each row exposes BOTH `occurred_at` (real-world event time) AND `recorded_at` (system row-insertion time) per Principle 3 — these can legitimately differ. `client_po_number` surfaces on every relevant row. Money in integer cents.

  • get_client_portal_finance_view

    Returns the five-panel client portal Finance tab data for the given client, panel by panel: (1) OUTSTANDING — outstanding cents, (2) PAST INVOICES — past invoices with a payable flag, (3) STATEMENT — the client's own self-serve statement generator, as statement_panel, (4) PAYMENT HISTORY — payments, (5) CREDIT ON ACCOUNT — credit balance + recent entries, plus whether the credit panel should display per the per-tenant tunability setting. Read-only — does not persist anything.

  • list_client_invoices

    Lists invoices for a specific client, ordered by issued_at descending. Optional status filter. Default page size 50, max 200. Money fields returned as integer cents. Read-only.

  • list_client_payments

    Lists payments recorded against a specific client's invoices, ordered by payment_date descending. Money fields returned as integer cents. Read-only.

  • request_statement_for_client

    Returns the on-demand statement aggregate for a client over a date range — the same data the client sees in their portal Finance tab when they pick a period. Includes opening balance, in-period invoices/payments/refunds, closing balance, outstanding, and per-PO breakdown. Money fields as integer cents. Read-only — does NOT persist a statement row.

  • set_client_invoicing_cadence

    Sets the invoicing cadence for a specific client. Controls what happens when a job for this client is completed.

  • get_late_fee_config_for_client

    Returns the resolved late-fee config for a client — mode (none|interest|flat_fee), flat fee amount (cents), interest rate (bps), grace period days, recurrence (once|monthly), and the source of the mode (tenant_default vs client_override).

  • get_client_credit_balance

    Returns the available (unapplied) credit balance for a client, broken down by credit. Available = amount − sum(non-deleted credit_applications). Amount in integer cents.

  • list_jobs_for_client

    Returns jobs for a specific client (across all their sites) within an optional date window. Without date_from/date_to it returns the client's jobs ordered by scheduled_date (earliest first, capped at limit); pass date_from/date_to — INCLUDING PAST DATES — to window the result, so this tool DOUBLES AS THE CLIENT JOB-HISTORY READ. A backfilled or completed PAST visit surfaces when you bound the window to its date — the 'upcoming' reading is only the default forward slice, never a hard filter, so do NOT conclude a job is 'not in client history' without querying its past date. Each row carries a denormalised service_name label alongside service_id, so status + service_name make the row self-describing. Each row also carries `contract_service_line_id` so jobs can be attributed to the specific recurring line that spawned them (disambiguates 2+ lines on the same service+site). Honours `tenants.scheduling_settings.client_can_see_contractor_details` — if false (default), the `contractor_id` field is stripped from the response.

Contractors (32)
  • list_contractors

    Returns contractors for the authenticated user's tenant with pagination. Excludes soft-deleted rows. Excludes test rows (is_test=true) by default — pass include_test:true to include them.

  • get_contractor

    Returns a single contractor by ID for the authenticated user's tenant. Returns business details, ABN, primary contact, sma_status, availability, capabilities, and denormalised compliance snapshot.

  • get_contractor_portal_link

    Returns the contractor's persistent portal URL (the /portal/contractor/{token} link) and its underlying portal token. Mirrors the client portal_token surface. The token is always one the portal guard ACCEPTS: it is minted when the column is null, ROTATED when the stored token has aged past the tenant's portal.link_ttl_days, and reused unchanged otherwise. With the TTL at its default of 0 the stored token is returned untouched, so link stability is unaffected. `rotated: true` on the response means the previous link was stale and has been replaced — any copy of that older URL stops working, which is the behaviour a tenant opts into by setting a TTL. The URL host is the tenant's canonical domain.

  • resend_contractor_portal_link

    Replace a contractor's portal link (the /portal/contractor/{token} URL) with a NEW one and email it to the contractor. The old link stops working immediately. The contractor-side twin of resend_client_portal_link. Recipients are the contractor's contacts flagged to receive work orders (falling back to the contractor's own email) — never a caller-supplied address; pass recipient_email only to send to ONE of those addresses. If the contractor has no address to send to, nothing is changed and a structured no_deliverable_contact error is returned, so a link is never cancelled without a replacement being delivered. Returns {contractor_id, portal_url, rotated, previous_token_invalidated, recipients, recipient_count, provider_message_ids, sent_at}.

  • list_contractor_agreements

    Returns agreements (SMAs etc.) for a contractor, newest first, with their current status (draft / staged_for_send / sent / accepted / declined). Each row carries the WP-SMA-TERMS-TRUTHFUL lifecycle fields: `supersedes_agreement_id` (the re-issue chain — a declined agreement is never overwritten, its amended successor points back at it), `responded_at` + `response_method`, and for declines the structured `declined_reasons` + `declined_note` (the contractor's own words). Each row also carries two DERIVED audit fields (bug 481d0095): `acceptance_provenance` — 'after_recorded_send' when the agreement was accepted after the platform recorded a send, 'without_recorded_send' when it was accepted with no send on record, and null when the agreement is not accepted at all — and the boolean `accepted_without_recorded_send`, true only for that second case. Recording an offline acceptance against a never-sent agreement is PERMITTED and always has been; these fields exist so an audit can tell such a signing apart from one the contractor was actually sent. `terms_schedule_snapshot` carries the commercial terms frozen at send (payment mode/trigger/days, completion evidence, insurance minimum, termination notice) — read it to see exactly what that agreement asserts.

  • list_contractor_quotes

    Returns contractor quotes — prices contractors have offered (or been asked for) on specific site/service work. Each row carries the amount AS WRITTEN plus its GST treatment, the canonical ex-GST figure the engine pays from, lifecycle status, and a `note` stating what happens next: a `submitted` quote is priced into work by accept_contractor_quote with YOUR client price — never ask the operator to restate an amount already on a recorded quote.

  • accept_contractor_quote

    Accepts a submitted contractor quote and prices work from it in one call. The quote's ex-GST amount becomes the CONTRACTOR rate; the CLIENT rate is YOUR decision — client_rate_cents states it outright, margin_percent_bps derives it from the quote for this acceptance, and omitting both falls back to the contractor's standing hidden_margin default. Target either an EXISTING job (job_id — e.g. a rate-null ad-hoc visit dispatch is refusing) or a NEW ad-hoc job (scheduled_date; site/service come from the quote). The margin arithmetic in the response is OPERATOR-SIDE ONLY and never reaches a contractor surface.

  • decline_contractor_quote

    Declines a contractor quote with a recorded reason. The quote keeps its history (never deleted); the contractor sees the outcome in their portal via an in-app notice. No email is sent — passing on a price is the operator's conversation to have in their own words.

  • list_contractor_compliance_documents

    Returns compliance documents (insurance certificates, ABN verifications, workers comp, etc.) for a contractor with their verification status and expiry dates.

  • get_contractor_compliance_summary

    Returns one contractor's compliance state for EVERY credential in the platform registry (insurance, licences, cards, registrations) with the resolved state and expiry date for each. States: none | pending_review | rejected | expired | expiring | current — the same lifecycle the assignment gate reads.

  • create_contractor

    Creates a new contractor (sub-contractor) for the tenant. Contractors are businesses that perform services on behalf of the tenant. Captures business_name, ABN, contact details, capabilities, and initial sma_status.

  • update_contractor

    Updates a contractor's business details, insurance, banking, availability, or capabilities. Most fields are operator-accessible; banking fields (`bank_bsb`, `bank_account_number`, `bank_account_name`) require architect persona.

  • configure_contractor_engagement

    Composite (front-of-house). One call to set — and read back — everything that shapes ONE contractor's experience: their rate model, whether they see their earnings and payable status on the portal, whether they can move their own job dates, payment terms, completion requirements, and how Work Orders are sent for a recurring client. Composes EXISTING writes only (update_contractor + configure_setting) so there is one right place for each knob.

  • contractor_visible_notes

    Read or write the CONTRACTOR-FACING free-text note on a payable or job. This note is DISTINCT from the operator-only `notes` column — the internal `notes` is never shown to a contractor; only this note crosses to the contractor portal (a deliberate, leak-safe channel for field-worker context). USE WHEN: recording a note a contractor SHOULD see (site access, what to bring, a heads-up); reading what note is set; auditing every contractor-visible note across the tenant (operation 'bulk_read'). DON'T USE WHEN: writing operator-internal commentary — use update_job / update_contractor `notes`. PRECONDITIONS: operation 'get'/'set' require entity_type ('payable'|'job') + entity_id in the caller's tenant; 'set' requires note_text (empty string clears the note). SIDE EFFECTS: 'set' updates contractor_visible_note on the matching row (scoped to the caller's tenant). 'get'/'bulk_read' are read-only.

  • create_contractor_contact

    Creates a contact person for a contractor. Each contact is a person with name, role, phone, email, and notification preferences.

  • add_contractor_service_capability

    Declares that a contractor is configured/qualified to deliver a given service by adding a row to the `contractor_services` capability map. This is the row the `assign_job` compliance gate checks (lib/scheduler/compliance.ts §5) — without it, assignment rejects with `no_capability`.

  • list_contractor_contacts

    Returns contacts for a contractor. Each contact is a person with name, role, phone, email, and notification preferences.

  • update_contractor_contact

    Updates a contractor contact's details or notification preferences. Only provided fields are changed.

  • create_contractor_agreement

    Creates a contractor agreement (SMA — Subcontractor Master Agreement) record in `draft` status. Agreements track the legal relationship between tenant and contractor. A draft must exist before propose_send_contractor_agreement can stage a send.

  • create_contractor_quote

    Records a contractor's quote — the price THEY get paid for specific work, exactly as they gave it (PDF, email, verbal, SMS). Capture the amount AS WRITTEN plus whether it was GST-inclusive; the canonical ex-GST figure is derived and stored. Recording an amount marks the quote `submitted`; the loop then closes with accept_contractor_quote, which takes YOUR client price and turns the quote into priced work — never ask the operator to restate numbers already recorded here.

  • create_contractor_compliance_document

    Creates a compliance document record (insurance certificate, ABN verification, workers comp, etc.) for a contractor. Documents track the contractor's regulatory compliance status. Newly created docs start unverified.

  • verify_contractor_abn

    Verifies a contractor's stored ABN against the Australian Business Register (ABR) and sets abn_verified=true when the ABN is ACTIVE. A collected ABN is already a satisfied prerequisite; this is the authoritative verification step.

  • get_contractor_queue

    Returns the contractor's pending work-order queue: every work order in `sent` or `requires_reacceptance` status assigned to the given contractor, enriched with site name, service name, scope, time-in-pending, and signing token. Excludes work orders for archived (soft-deleted) clients.

  • record_contractor_bill

    Records a contractor's bill (tax invoice) as a payable. The BACKFILL / single-shot door — for the historical paper pile where NO accrual exists yet. For an ongoing multi-job contractor invoice claiming EXISTING payables, use record_bill (WP-CONTRACTOR-BILLS) instead — this tool MINTS a payable and would double-count. Bills land as payable rows (D2, superseded for ongoing work — see record_bill). GUARDED: refuses with code open_accruals_exist when the contractor already holds open job-pipeline accruals. GST is STORED as printed on the paper, never derived (D3). Can record the bill as already-paid (legacy backfill, external payment, bank feed) or as unpaid (approved, awaiting payment).

  • get_contractor_bills

    Returns all bills (payable rows with bill metadata) for a contractor, optionally filtered by period. Each row includes bill_number, bill_date, amount, GST, status, paid_via, and document attachment status.

  • get_contractor_finance_view

    Full financial view for a contractor: summary (owed now, paid this AU FY, last paid date, bill count), 12-month money-out chart data, and the bills ledger with status + evidence. This is the agent's version of the contractor profile financial tab.

  • list_jobs_for_contractor

    Returns all jobs assigned to a specific contractor within a date range, optionally filtered by status. Contractor-portal-style view (no client legal scope, no client rate). Sorted by scheduled_date ascending.

  • get_contractor_schedule

    Returns a contractor's own upcoming schedule: jobs by date plus aggregate stats. Excludes cancelled and terminal (completed/approved/missed) jobs. No client legal scope, no client rates.

  • suggest_contractors_for_job

    Returns the contractors that PASS the compliance gate for this job's service (`candidates`), the contractors that FAIL it (`blocked` — each with the failure reason, including contractors with no capability registered for the service), plus the contractor preferred for the underlying CSL (if any). When the tenant's delivery model is `hybrid` or `self_perform`, the response also includes a `team_members` array of active persons from the workforce roster (WP-WORKFORCE-FOUNDATION) — each with `id`, `full_name`, `position`, `status`. Team members are assigned via assign_job with `team_member_id` instead of `contractor_id`. Every entry carries `display_name` (trading-name-first, tunable via the `directory.contractor_display_name` setting) alongside the raw `business_name`, and `is_internal` — true when that row is the tenant's OWN labour crew (do_job_myself provisions it) rather than a third-party subcontractor. Read `is_internal` to tell own crew from a sub; never infer it from the name, which a tenant may change. Excludes test rows (is_test=true) from both `candidates` and `blocked` by default — pass include_test:true to include them. Every entry — candidate AND blocked — also carries `prospective_rate`: what that contractor would be paid on THIS job, resolved from their own rate model against the job's client rate ({ derivable, rate_cents, model, bps, source, margin_disclosed, reason, note }). A blocked contractor's cost is still a fact. `derivable: false` names the missing input rather than guessing. The job's `client_rate_cents` is returned alongside so margin is one subtraction. MVP returns binary pass/fail with no ranking; v2 adds geographic proximity + workload scoring (deferred).

  • contractor_portal_bulk_reschedule_day

    Moves ONE contractor's whole day of visits to another day in a single action — the day-level answer to a rained-out morning, where the per-job 'Change date' path would be N separate journeys. Resolves the job set BY contractor and date, so an explicit `job_ids` list can only ever narrow that contractor's own day; ids belonging to another contractor, tenant or day come back in `refused` and are never touched. Honours the tenant's contractor schedule-change mode: `yes` moves the visits directly; `request_only` moves NOTHING and instead raises one grouped schedule_change_request per visit, every row stamped with the same `batch_id` and the same reason verbatim, so the operator sees ONE decision carrying one shared cause (list_schedule_change_requests collapses the batch to a single row); `no` refuses every visit.

  • preview_send_contractor_agreement

    Previews EXACTLY what a Subcontractor Master Agreement send will contain — the email subject and body as they will be sent, the Schedule 1 clauses the contractor will sign (payment, completion evidence, insurance minimum, termination), and a readiness assessment of the contractor's setup. Read-only: stages nothing, sends nothing.

  • record_contractor_agreement_response

    Records a contractor's real-world response to a sent Subcontractor Master Agreement — accepted or declined — with HOW it arrived (email reply, phone, in person) and, for declines, WHICH clauses they objected to as structured reasons plus their own words. Transitions the agreement (sent/staged_for_send → declined or accepted), syncs the contractor's sma_status ('declined' or 'signed'), expires the signing token on a decline so the refused instrument is no longer signable, emits `contractor_agreement.responded`, and writes the audit row.

Contracts & Services (21)
  • list_services

    Returns the service catalog for the authenticated user's tenant. Services are CLIENT-AGNOSTIC and CONTRACTOR-AGNOSTIC — they describe what work is offered, not who it's for. Excludes soft-deleted rows by default. Each row carries `audience` (residential|commercial|both — MATCH THIS to the job's setting when picking a service; never use a residential service for commercial work) and `public_summary` (the customer-facing pitch voice).

  • suggest_service_for_job

    Given a free-text job description (and optionally client_type and a category hint), returns the tenant's catalog services ranked by fit — each with a short legal_scope summary, a deterministic fit score, why it fits, and caveats naming any scope mismatches — plus a list of near-duplicate catalog entries. Lets an agent pick the correct service without reading every legal_scope by hand, and avoids printing the wrong Scope of Services on a client PDF. Ranking is a PURE, DETERMINISTIC token-overlap function — the tool never calls an LLM; the calling agent is the LLM that makes the final pick from the ranked candidates.

  • list_service_credential_requirements

    Reads the credential requirements configured for services — the missing counterpart to set_service_credential_requirement, and the way to answer 'can this contractor take this work?' BEFORE creating a job or recording a verdict.

  • list_contract_service_lines

    Returns the rate schedule lines (per site/service/contractor) for the tenant. Optionally filtered by site_id, service_id, contractor_id, or client_id (resolved via the client's sites). Each row carries the client_rate_cents, contractor_rate_cents, frequency, active months, and active flag, plus denormalised human-readable labels: service_name, frequency_name, human_description, contractor_name (null when no contractor assigned), site_name, client_name, and client_id (resolved via the line's site). human_description is the active recurrence rule's authoritative cadence label (e.g. 'Every week on Thursday') and should be preferred over frequency_name — the catalog template name, which can lag a day_of_week override (bug 0d32f07b). A seasonal line's months restriction is appended to human_description in the SAME wording in every mode (e.g. 'Every week on Thursday (Mar–Nov only)'), so summary and full rows never disagree about what was sold (bug dadf6acb). UUID fields are retained for joins. contractor_name is the display name resolved trading-name-first (shared with the operator UI, tunable per tenant via directory.contractor_display_name), not the bare business_name. Full rows also carry the test-data flags is_test + is_test_preserved. For large result sets pass summary:true (the exact lean key set: id, client_id, client_name, site_name, service_name, line_label, human_description, frequency_name, active_months, client_rate, contractor_id, is_active — line_label and active_months are in the lean set because without them two DIFFERENT scopes on the same site+service read as identical duplicate rows, bug dadf6acb) or count_only:true (just { count_only, count }, no rows) — mirrors list_jobs.

  • get_contract_service_line

    Returns full details for a single contract service line, including recurrence data (rrule_string, human_description — the authoritative cadence label, preferred over frequency_name which can lag a day_of_week override per bug 0d32f07b — next N occurrences, plus the raw materialised recurrence — program_definition, rdates, exdates — for explicit_months/program cadences), live job counts by status, and the client portal guardrail counters (client_reschedule_count, client_defer_count — quarterly counters incremented by client-initiated reschedules/defers, checked against max_reschedules_per_quarter). Also returns contractor_name (display name resolved trading-name-first, shared with the operator UI and list_contract_service_lines) and the test-data flags is_test + is_test_preserved.

  • create_service

    Creates a generic, reusable service in the tenant's catalog. Services are CLIENT-AGNOSTIC and CONTRACTOR-AGNOSTIC — they describe what work is offered, not who it's for or who delivers it.

  • update_service

    Updates fields on a generic catalog service. Only provided fields are changed. The same content rules as create_service apply: services are CLIENT-AGNOSTIC and CONTRACTOR-AGNOSTIC.

  • set_service_credential_requirement

    Declares (or retires) a credential that contractors MUST hold — verified and unexpired — before they can be assigned to a service. `required: true` adds the requirement; `required: false` retires it. Idempotent: re-setting the same value is a no-op, and a retired requirement can be revived without losing history.

  • archive_service

    Soft-deletes a service from the catalog (sets `deleted_at`). The service row remains in the database for audit history and FK references from existing contract_service_lines / jobs, but is excluded from active listings.

  • create_frequency

    Creates a new frequency in the tenant's catalog. Frequencies define scheduling patterns (e.g., weekly, fortnightly, 4-weekly) used by contract_service_lines to determine job recurrence.

  • update_frequency

    Updates an existing frequency's fields. Only provided fields are changed.

  • archive_frequency

    Soft-deletes a frequency from the catalog (sets `deleted_at`). The frequency row remains in the database for audit history and FK references from existing CSLs, but is excluded from active queries.

  • create_contract_service_line

    Creates a contract service line — THE place for client-specific pricing. Links a site to a service with a frequency, client_rate_cents, and optional contractor_rate_cents. This is where per-client, per-site pricing lives.

  • update_contract_service_line

    Updates a contract service line's rates, active status, contractor assignment, active months, service (in-place swap), or recurrence inputs (day_of_week, week_of_month, anchor_date). This is where rate changes, seasonal adjustments, service swaps, and schedule edits are made.

  • add_adhoc_service

    Adds an ad-hoc service to a client — creates the job, bills it into the client's normal invoicing cadence, and creates the contractor payable if applicable. One composite call replaces the manual sequence of create_job + set_client_invoicing_cadence + create_invoice + create_payable.

  • list_service_requests

    Lists inbound service requests — what CLIENTS have asked for through their portal, before any proposal exists. This is the intake queue: the front door of the pipeline.

  • get_service_request

    Full detail for ONE inbound service request, plus the triage actions currently available on it.

  • triage_service_request

    Marks an inbound service request as UNDER REVIEW — the operator has seen it and is working on it. The client sees the status move in their portal, which is the whole point: it is the difference between 'they got it' and silence.

  • decline_service_request

    Declines an inbound service request with a reason the CLIENT WILL READ in their portal. Write it as you would say it to them — 'We don't service that area' is fine; 'n/a' is not.

  • create_proposal_from_service_request

    Turns an inbound service request into a DRAFT proposal and links the two — the intake-to-quote hop. The request moves to `quoted`; the proposal inherits the client, the site (when the request names one), the client PO, and the request's description as notes.

  • create_recurring_service

    Set up a recurring service for a client at a site — anything from a simple fixed cadence to a multi-phase PROGRAM with intervals that change over the year. Creates a contract service line + a materialised recurrence rule, spawns the look-ahead jobs, and emits job.spawned / schedule.changed so the rest of the chain (dispatch → work orders → invoicing) flows automatically. Two-phase: confirm=false PREVIEWS the exact dates (no writes); confirm=true creates.

Jobs (23)
  • list_jobs

    Returns jobs (work orders, scheduled visits) for the tenant. Optionally filtered by status, site_id, contract_service_line_id, contractor_id, or date range. ONE RECURRING SERIES' VISITS: pass `contract_service_line_id` (tool upgrade 3784a075) — this is the filter get_job's description points at. Do NOT substitute site_id for it: a site commonly carries several service lines, so a site filter returns other series' visits too. Excludes test rows (is_test=true) by default — pass include_test:true to include them. For large result sets use `summary: true` (lean per-job projection + by_status rollup) or `count_only: true` (counts by status, no rows) to stay under the response size limit — recurring schedules make tenant-wide job lists large fast. Full rows include the reschedule fields (is_rescheduled, original_date, rescheduled_from, rescheduled_to, reschedule_reason) — the same ones get_job returns — so you can confirm a single-move-vs-cascade across many jobs in one call instead of N get_job calls (tool upgrade ee800883). READ `original_date`, NOT `rescheduled_from`, when you need the recurrence occurrence a visit belongs to: `original_date` is stamped once at spawn and preserved through every move, whereas `rescheduled_from` holds only the LATEST hop, so after a second reschedule it names an intermediate date that is not an occurrence at all (WP-SPAWN-OCCURRENCE-IDENTITY). Two live jobs sharing a `(contract_service_line_id, original_date)` pair are one occurrence billed twice. Full rows also carry `contractor_rate_resolved` — READ THAT, not the bare `contractor_rate` column, for what the contractor actually gets paid: the payable resolves the contract service line's rate FIRST and only falls back to the job's. `diverges: true` means the two stored columns disagree and the LINE is winning — informational, not a stale rate to repair. `committed: false` means the amount is still a projection. Where NEITHER column carries a rate, the holder's own rate model derives one and `derived: true` says so — `cents: null` then means it genuinely cannot be stated, and `reason` names the missing input. `summary: true` keeps `contractor_rate_resolved` as just `{cents, source}`; `count_only: true` returns no rows at all.

  • get_job

    Returns a single job by ID with full details including status, site, contractor, schedule, notes, completion photos, and completion data.

  • create_job

    Creates a new job (work order) for a site/service on a scheduled date. Jobs represent individual instances of work to be performed. Should reference a contract_service_line where one exists. The job starts in `scheduled` status (or `unscheduled` if no date is provided).

  • update_job

    Updates a job's status, schedule, contractor assignment, or notes. Valid statuses: scheduled, dispatched, completed, approved, invoiced, paid, on_hold, cancelled, missed, partial. General-purpose; specialised scheduler tools (assign_job, dispatch_job, approve_job, reschedule_job, cancel_job) are preferred when they apply.

  • invoice_job_separately

    Bills ONE completed visit on its own invoice, safely excluded from the client's period-final consolidated invoice. Previews by default.

  • backfill_completed_job

    Records an already-performed job when the contractor's bill arrives after the fact. ONE CALL creates the completed job + contractor payable with the attested GST-INCLUSIVE bill amount (84f7204f). Returns a suggested invoice line for billing the client — suggested_invoice_line.amount_cents is the CSL client_rate_cents (ex-GST); pass amount_basis:'gst_exclusive' to create_invoice/add_invoice_line so 10% GST is added on top (treating it as GST-inclusive under-bills the client). The suggested_invoice_line is self-describing: it echoes amount_basis:'gst_exclusive' and the source contract_service_line_id (null when billed from a direct rate), so you can spread it straight into add_invoice_line/create_invoice lines[] without re-deriving the basis from this prose.

  • assign_job

    Assigns a contractor to a job. Runs the compliance gate (SMA signed, insurance current, capability matches service, availability). On a hard-gate failure the assignment is blocked and the reason is surfaced. Soft warnings are returned but do not block.

  • dispatch_job

    Transitions an assigned job from `scheduled` to `dispatched`. Sets `work_order_sent_at = NOW()`. Emits `job.dispatched`. Generic dispatch — for the email-bearing work-order send, use the propose_send_work_order / approve_send_work_order pair.

  • approve_job

    Approves a completed (or partial) job — admin sign-off step that downstream invoicing consumes. Sets `status='approved'`, `approved_at=NOW()`, `approved_by=actor`. Emits `job.approved` with the full payload (contractorId, scheduledDate, clientId, siteId, cslId, approvalNotes, client_po_number per the PO cascade).

  • reschedule_job

    Moves a single job to a new date and captures a reason. Validates the new date shape and that the job is not in a terminal status (completed/approved/invoiced/paid/cancelled). Sets `is_rescheduled=true` and increments the per-CSL reschedule counter when triggered from the client side. Emits `job.rescheduled`. When `schedule_change_request_id` is supplied, atomically closes that pending request as 'approved' and links it in the audit trail. Any OTHER pending schedule_change_requests on the job are auto-closed as 'superseded' (architect-ratified 2026-06-06) so the operator queue never accumulates stale rows.

  • cancel_job

    Cancels a job and captures a reason. Terminal status transition — cancelled jobs are not respawned by the engine. Emits `job.cancelled`.

  • unassign_job

    Removes the assigned contractor from a job and returns it to the unassigned scheduled pool (the inverse of assign_job). Emits `job.unassigned`.

  • uncomplete_job

    Reverts a completion — the undo for marking a job done (Housecall-Pro 'Unfinish'). Returns the job to 'dispatched' and clears the completion timestamps. Emits `job.completion_reverted`.

  • hold_job

    Parks a job on hold with a reason (the visit is paused, not cancelled). Emits `job.held`.

  • resume_job

    Resumes a job that is on hold, returning it to the 'scheduled' pool so it can be re-dispatched. Clears the hold reason. Emits `job.resumed`.

  • do_job_myself

    Self-delivery composite (WP-SCHED-GOLD-2): assign a scheduled job to the tenant's OWN internal crew and dispatch it, in ONE call. The operator is doing the work themselves rather than subbing it out. Emits one `job.assigned` + one `job.dispatched`.

  • decline_job

    Contractor declines an assigned job after acceptance. Captures `decline_reason` and clears `contractor_id` so the job returns to the unassigned queue. Status transitions to `declined`. Emits `job.declined`.

  • list_unassigned_jobs

    Returns unassigned jobs needing dispatch (`status='scheduled'` OR `status='declined'`) with `contractor_id IS NULL` — the admin dispatcher's unassigned queue. Declined jobs re-enter this queue for re-dispatch (decline_job clears contractor_id). `decline_reason` is included so the dispatcher knows why a job is back. Filterable by date range (defaults anchor to TENANT-TIMEZONE today, never UTC — WP-SCHED-GOLD-0). Sorted by scheduled_date ascending. Excludes test rows (is_test=true) by default — pass include_test:true to include them.

  • bulk_assign_jobs

    Assigns N jobs to one contractor in a single call. Compliance gate runs once per (contractor, service_id) — jobs that fail capability or other gates are returned in the `failed` list with their reason. All passes are committed; nothing rolls back on a partial failure.

  • repair_orphaned_job_assignments

    One-tap remediation sweep for jobs orphaned by the pre-fix CSL-respawn path (bug 0f4bb3e2 / e66914a0). Restores each recurring line's contractor onto its scheduled/declined jobs that lost their assignment, via the single assign write-path (compliance gate + CSL-rate inheritance). Idempotent: re-running once the orphans are repaired is a no-op.

  • bulk_reschedule_jobs

    Moves N jobs to a single new date with the same reason. Each job's lifecycle is gated independently — completed/approved/invoiced/paid/cancelled jobs are returned in the `failed` list. Emits one `job.rescheduled` event per successful move.

  • run_job_spawning

    On-demand preview and execution of the recurring-job spawning engine. Two-phase: call with dry_run=true (default) to preview what jobs would be created, then dry_run=false with the returned confirm_token to create them.

  • run_missed_job_detection

    On-demand preview and execution of the missed-job detection sweep — the pass that transitions past-due, non-terminal jobs to status 'missed' and emits job.missed. Two-phase: call with dry_run=true (default) to see exactly which jobs WOULD be flagged, then dry_run=false with the returned confirm_token to flag them.

Work Orders (7)
  • list_work_orders

    Returns work orders for the tenant. Optionally filtered by status, job_id, contractor_id, scope, or contract_service_line_id. Status values: sent, accepted, declined, rejected_after_acceptance, expired, cancelled, withdrawn, requires_reacceptance. Each row carries its WP-WO-01 scope (single_job / job_set / ongoing_series / ongoing_series_set), `bound_job_ids` (junction bindings for job-bound scopes, null for series scopes) and `bound_contract_service_line_ids` (every recurring line a series work order binds — one for ongoing_series, several for ongoing_series_set; null for job-bound scopes).

  • get_work_order

    Returns a single work order by ID with full details including signing token, acceptance record, and signed PDF URL. WP-WO-01 scope model: the response includes `scope` (single_job / job_set / ongoing_series / ongoing_series_set) plus the binding — `bound_jobs` (junction-bound job rows) for job-bound scopes, `contract_service_line` (the bound series) for ongoing_series, and for BOTH series scopes `bound_contract_service_line_ids` + `contract_service_lines` (every line the work order binds — several for ongoing_series_set, whose `contract_service_line` is null because one signature covers them all).

  • get_work_order_aging

    Returns pending work orders sorted by age (oldest first), with time-in-pending computed from `sent_at`. Filters by status and/or contractor. Excludes work orders for archived clients. Use to identify stale work orders that need follow-up.

  • approve_send_work_order

    Approves a staged work-order send and immediately dispatches the email to the contractor with a portal-link CTA. Mints the signing_token, creates the `work_orders` row, sends the branded email, and emits `work_order.sent`. The contractor clicks the link, reviews the work order, and digitally accepts via the portal. The response surfaces `recipients` (the full set the send fanned out to — every receives_work_orders contact, de-duped) and `recipient_count` alongside the back-compat scalar `recipient_email` (= recipients[0]), so you can confirm the fan-out from the tool output instead of an inbox check.

  • record_work_order_acceptance

    Records an out-of-band work-order acceptance (e.g. contractor accepted verbally or via email, and the operator is back-filling the system). Creates an immutable `work_order_acceptances` row and flips `work_orders.status` to `accepted`. Handles all three scopes (WP-WO-01): `single_job` / `job_set` acceptance binds the junction-bound job(s) and transitions them to `dispatched`; `ongoing_series` acceptance binds the contract service line AND flows onto its jobs (flowSeriesAcceptanceOntoJobs): EVERY still-scheduled job on the CSL is assigned to the accepting contractor and transitioned to `dispatched` (jobs already assigned to a DIFFERENT contractor are left alone), the spawner mints future occurrences pre-assigned + dispatched while the acceptance stays live, and the forward-view snapshot is stored. `ongoing_series_set` (one work order binding SEVERAL lines) does the same for EVERY bound line: the snapshot stores each line's projection and instructions, and the response + event carry `contract_service_line_ids`.

  • cancel_work_order

    Cancels an in-flight work order and invalidates its acceptance link. Soft transition: the row is KEPT (status='cancelled'), the signing_token is expired so the portal accept link stops working, and `work_order.cancelled` is emitted. Once cancelled, the bound job's in-flight guard clears, so `propose_send_work_order` can stage a fresh send — this closes the dead-end where a sent WO blocked re-issuing after a contractor reassignment.

  • propose_send_work_order

    Stages a work-order send. Accepts EXACTLY ONE of `job_id` (single job), `job_ids` (a set of jobs for the same contractor, max 100), `contract_service_line_id` (an ongoing recurring series), or `contract_service_line_ids` (SEVERAL ongoing recurring series for the same contractor at the same site, max 20 — one work order, one email, one signature) — the work-order scope (single_job / job_set / ongoing_series / ongoing_series_set) is inferred from which identifier you supply.

Proposals (17)
  • create_invoice_from_proposal

    Composite: creates a DRAFT invoice directly from an ACCEPTED proposal. Resolves the client from the proposal, copies each live proposal line to an invoice line (proposal amounts are EX-GST by schema convention — the gateway adds 10% GST and stores canonical GST-inclusive amounts), inherits the proposal's client PO, and computes due_date from the payment-terms cascade (client terms → tenant default) unless an explicit due_date is supplied. Does NOT send — the draft goes through the normal propose_send_invoice → confirm_pending_operation → approve_send_invoice chain.

  • list_proposals

    Lists proposals for the tenant, with optional filters. Returns proposal header info including status, client, site, and dates. Excludes soft-deleted rows by default.

  • get_proposal

    Gets a single proposal with full detail: header, all lines (with service/frequency names AND their resolved cadence), and acceptance records.

  • preview_proposal_investment

    Preview the HONEST, frequency-aware investment summary for a proposal BEFORE sending — the SAME figures the operator view, the signed-agreement PDF, and the client accept page render (one shared calculator, so the preview matches the document, no drift).

  • list_proposal_acceptances

    Lists proposal acceptance records for audit/history. Returns core fields by default; set `include_snapshot=true` to include the full proposal_snapshot JSONB (large). Audit fields include ip_address, user_agent, content_hash, pdf_url, pdf_generated_at, confirmation_email_sent_at, and retention_class.

  • create_proposal

    Creates a new proposal in `draft` status for a client at a specific site. After creation, add lines with add_proposal_line. Auto-assigns the next proposal_number for the tenant.

  • update_proposal

    Updates the header fields (intro message / notes, valid_until date, site) on a draft proposal. Only allowed when proposal status is `draft` — same constraint as update_proposal_line. For line item changes use update_proposal_line. For status changes use propose_send_proposal or cancel_pending_operation.

  • add_proposal_line

    Adds a line item to a draft proposal. Each line links a service (and optionally a frequency) with pricing in integer cents. Only allowed when proposal status is `draft`.

  • update_proposal_line

    Updates a proposal line's price, description, quantity, or display order. Only allowed when the parent proposal is in `draft` status.

  • remove_proposal_line

    Soft-deletes a line from a draft proposal (sets `deleted_at` on the line row). Only allowed when proposal status is `draft`.

  • record_proposal_acceptance

    Records that a proposal has been accepted. Creates a `proposal_acceptances` record (immutable legal evidence), updates the proposal status to `accepted`, and runs the SAME activation cascade as the portal accept path: client lead→active, contract service lines stood up from the proposal's frequency AND cadence lines (line facts carried), recurrence rules materialised, and look-ahead jobs spawned synchronously. For email-reply acceptance, captures the literal acceptance text as legal evidence.

  • decline_proposal

    Records that the client DECLINED a sent proposal (status `sent` → `declined`), closing it out of the live pipeline honestly instead of leaving it dangling as sent forever. The proposal is retained (not archived) as pipeline history; its accept link stops rendering a signable agreement.

  • bulk_archive_proposals

    Soft-deletes multiple proposals in a single call. Accepted proposals are IMMUTABLE and are skipped (not archived). Cascade-cancels any associated pending/staged operations for archived proposals.

  • clone_proposal_with_amendments

    Supersedes a sent proposal with an amended copy in one atomic call. Archives the source, creates a new draft with the same client/site, copies all lines (applying optional price/quantity/description/frequency/CADENCE amendments — a mid-flight cadence change is ONE call via line_amendments[].new_cadence), and links source → new via `superseded_by`.

  • get_amendment_proposal_history

    Returns the FULL history of a single amendment proposal: the proposal row itself, the domain events that raised and acted on it, and the audit-log narrative — merged into one chronological timeline.

  • list_amendment_proposals

    Lists amendment proposals for the tenant, with optional filters by status, direction, and CSL.

  • action_amendment_proposal

    Accept or decline an amendment proposal. Composite operator action for the negotiation surface.

Invoices (39)
  • list_invoices

    Returns invoices for the tenant. Filter by client_id, status, since (ISO date), or `search`. `search` matches the invoice number (exact or prefix, e.g. 'INV-26-0033' or 'INV-26') OR the client's legal/trading name (case-insensitive substring) — a term that matches nothing returns an EMPTY list, never the newest invoices, so an empty result is a trustworthy 'no such invoice'. Returns totals as integer cents plus lock_version, is_catch_up_invoice, client_po_number, and the LIVE overdue position — `display_status` (the stored status, except a past-due 'sent' or 'partially_paid' invoice that still owes money displays 'overdue') and `is_overdue` (true whenever display_status is 'overdue', stored or derived) — on every row, judged against `as_of_date` (today in the TENANT's timezone). `status:'overdue'` is DERIVED-INCLUSIVE: it returns stored-overdue invoices AND sent or partially_paid invoices already past their due date (bug 48e73018 — a short-paid invoice stays on the chase list), so chasing late payers never misses an invoice the once-daily sweep hasn't flipped yet (bug 7218f238). The stored `status` field still reports exactly what is persisted; every other status filter keeps exact stored-enum semantics. Every row also carries `client_name` — the name the OPERATOR knows the business by (its trading name by default, e.g. 'Gladstone Optical Centre'), which is what you should use when telling a human who an invoice belongs to — alongside `client_legal_name` (the registered entity, e.g. 'CAROWAY ENTERPRISES PTY LTD', which is what prints on the tax invoice). Ageing is pre-computed in the same tenant timezone: `days_overdue`, `days_until_due`, `draft_age_days` and `due_phrase` ('14 days overdue', 'due in 3 days', 'draft · 6 days old') — read them rather than doing date arithmetic yourself. Which name leads is per-tenant: `directory.entity_display_name_preference`. By default hides `is_test=true` rows; pass `include_test=true` to surface them. DATES ARE SET AT SEND — automatically. A draft's issue date and due date are provisional. The first real send re-stamps the issue date to the send date (tenant timezone) and the due date to the client's payment term counted from that day, BEFORE the PDF is rendered, so the client always receives a correctly dated invoice. Do NOT hand-adjust a draft's dates to 'today' before sending: setting a date via update_invoice PINS it and the send keeps exactly what you set (that is the deliberate opt-out for back-dated or agreed dates). Tenant tunable: finance.invoicing.dates_on_send (issue_and_due default | due_only | none). Every draft row carries `dates_provisional` (true unless `dates_pinned`): to send the drafts, run the propose → confirm → approve chain on each and let the send date them.

  • get_invoice

    Returns a single invoice by ID with its lines, totals (integer cents), lock_version, status, ATO fields (supplier/buyer), client_po_number, dates, is_catch_up_invoice flag, and live payment position: paid_cents (Σ non-voided payment allocations − attributed refunds), refunded_cents (refunds prorated by allocation share), credit_applied_cents (Σ applied client credits), and outstanding_cents (total − paid_cents − credit_applied_cents − Σ sent credit notes, floored at 0 — the canonical FOUR-term basis in lib/finance/outstanding.ts; the sent-credit-notes term was ratified 2026-06-18 by bug 48865c9a) — one call answers "how much is left to collect?", refund-aware (bug 346589f7). A SENT CREDIT NOTE SETTLES AN INVOICE AND IS NOT RETURNED AS ITS OWN FIELD: it reduces outstanding_cents through that fourth term, so a credit-note-settled invoice legitimately reads status 'paid' with paid_cents 0, credit_applied_cents 0 and outstanding_cents 0 — the figures are correct and nothing is broken. Note credit_applied_cents counts applied CLIENT CREDITS, a different instrument that does NOT include credit notes. Call list_credit_notes for the invoice to see which credit note settled it (tool upgrade 9f906eca). Also returns the LIVE overdue position — `display_status` (the stored status, except a past-due 'sent' or 'partially_paid' invoice that still owes money displays 'overdue'), `is_overdue` (true whenever display_status is 'overdue', stored or derived), and the `as_of_date` it was judged against (today in the TENANT's timezone). Read `display_status`/`is_overdue` to answer "is this late?": the stored `status` flips to 'overdue' only on a once-daily sweep and lags by up to 24h (bug 7218f238). `status` still reports exactly what is persisted. Also returns the EFFECTIVE recipient identity — effective_recipient_abn, effective_recipient_name, recipient_abn_source ('header'|'client') — resolved as invoice.buyer_abn then the client's ABN, mirroring the rendered tax invoice + ATO send-gate, so a recipient-ABN compliance read needs no PDF render (tool upgrade 0d134b9a). DATES ARE SET AT SEND — automatically. A draft's issue date and due date are provisional. The first real send re-stamps the issue date to the send date (tenant timezone) and the due date to the client's payment term counted from that day, BEFORE the PDF is rendered, so the client always receives a correctly dated invoice. Do NOT hand-adjust a draft's dates to 'today' before sending: setting a date via update_invoice PINS it and the send keeps exactly what you set (that is the deliberate opt-out for back-dated or agreed dates). Tenant tunable: finance.invoicing.dates_on_send (issue_and_due default | due_only | none). On a DRAFT the response carries `dates_provisional` (true unless pinned), `dates_pinned`, and a `dates_note` saying so — read them before reasoning about a draft's issue/due dates.

  • get_payment

    Returns a payment by ID with its allocations against invoices (m:n via payment_allocations). Amounts in integer cents. `payment_date` is the real-world bank/cash receipt date (per Finance Engine Principle 3). `reference` is the external payment reference stored at record time (propose_record_payment's `external_reference` param — bank transaction ID, EFT reference, cheque number).

  • list_payments

    Returns payments for the tenant, filtered by invoice_id (via allocations), client_id, since (ISO date). Amounts in integer cents. By default hides `is_test=true` rows. Each row includes `reference` (the external payment reference stored at record time — bank transaction ID / EFT reference / cheque number), matching get_payment's shape so reconciliation reads it directly without an N+1 get_payment lookup.

  • record_monthly_tax_invoice

    Records a supplier's MONTHLY TAX INVOICE — Stripe's is the case this exists for (Stripe dashboard → Settings → Plans and fees → Invoice history; one per calendar month, issued ~5th of the next month) — as EVIDENCE for the fee expenses already booked by settle_stripe_payout. It is NEVER a second expense: it creates 0 expense records and moves the P&L by 0.

  • get_monthly_tax_invoice_check

    The MONTHLY CHECK for a supplier's monthly tax invoice — Stripe's by default: for one service month, compares the fee expenses on the books (booked by settle_stripe_payout) with the recorded monthly tax invoice, to the cent, and returns every gap SIGNED (books − invoice): GST-inclusive `total`, `gst`, `exGst`, and — when the invoice's lines were recorded — `paymentCount` and `gross` of settled card payments. A mismatch is REPORTED, never adjusted. Also lists `unevidencedExpenseIds` (fee expenses the invoice's document is not linked to), and, reading the tenant's Stripe account, `stripe.unsettledCharges` / `stripe.unsettledPayoutIds`: that month's charges sitting in payouts not yet settled — usually why the books are short of the invoice. `status`: 'reconciled' (total AND GST gap 0), 'gap', or 'awaiting_invoice' (none recorded yet — the invoice arrives ~5th of the next month; this never blocks settling).

  • attest_payment

    Record how a payment physically landed: the BANKED portion (will appear on this account's bank feed) and the UNBANKED portion (cash retained, or deposited to another account). The invariant is banked_cents + unbanked_cents == the payment's amount. Example: a $1,320 payment settled $420 by bank transfer and $900 cash → attest banked_cents 42000, unbanked_cents 90000, unbanked_method 'cash'. This lets the $420 bank line reconcile to the banked portion instead of nagging as an underpayment. It is a PROVENANCE SIDECAR — it does NOT change the payment amount, its allocation, or any GST, and executes no money movement (Financial Operations Test: PASS). Idempotent per payment (re-attesting overwrites the split).

  • get_payment_attestation

    Return the provenance attestation for a payment — the banked vs unbanked split (banked_cents, unbanked_cents, unbanked_method, note) recorded by attest_payment — or null if the payment has not been attested (provenance unknown / assumed banked).

  • match_bank_credit_to_invoice

    Matches a bank CREDIT (money in) to an invoice — the credit-side mirror of match_bank_debit_to_payable, and the same action the Reconcile screen performs. Records the customer payment against the invoice on the REAL bank value date, marks the deposit reconciled and linked to that payment, and teaches the client's bank alias so this payer is recognised next time.

  • match_bank_credit_to_invoices

    Matches ONE bank CREDIT (money in) across SEVERAL invoices of the same client, with an explicit amount in integer cents for each invoice — the split version of match_bank_credit_to_invoice, and the same action as the Reconcile screen's split mode. Records ONE customer payment on the deposit's REAL bank value date, allocated across the listed invoices (each invoice's status recomputes: 'paid' when its balance reaches zero, else 'partially_paid'), marks the deposit matched to that payment, and teaches the client's bank alias.

  • link_bank_credit_to_existing_payment

    Links an unmatched bank CREDIT to a payment that has ALREADY been recorded, WITHOUT creating any new money. Stamps the existing payment with the deposit it corresponds to, marks the deposit reconciled and linked to that payment, and learns the client's bank alias — but inserts NO payment and NO allocation, and leaves the invoice's status, total, paid position and lock_version untouched.

  • list_invoice_state_changes

    Returns chronological state-transition rows from `audit_log` for invoices in this tenant (entity_type='invoice', filtered to state-transition verbs: created, send/sent, paid, partially_paid, overdue, void/voided, updated). Optionally filter by invoice_id and since (created_at). Read-only temporal query per Finance Engine Principle 4.

  • issue_invoice

    Makes a DRAFT invoice a real receivable WITHOUT emailing the client — it starts appearing on statements, it ages, and it can be included in a pay-all link. The client is told separately afterwards, by whichever communication you choose (an invoice send, a consolidated statement, or nothing). Returns `client_notified: false` explicitly, because issuing is not sending.

  • mark_invoice_paid

    Marks a client invoice as paid by recording a SYNTHETIC reconciling payment row (source='manual_mark_paid', payment_method='other') plus a payment allocation equal to the invoice's outstanding balance, then transitioning the invoice to paid. Manual-override / paper-only reconciliation path. The canonical operator workflow for received money is record_payment (composite, propose+approve gated, records the payment + allocation + transitions invoice in one staged operation). Use this tool only for a paper-only / manual reconciliation where the receipt is NOT separately imported from an external ledger.

  • list_blocked_invoices

    Lists invoices the platform REFUSED to raise automatically because the duplicate-invoice guard suspected a duplicate — completed work that is sitting UNBILLED and needs a human decision. Each entry names the client, the period, the amount left uninvoiced, the job ids, and the existing invoice it was mistaken for. Entries are re-checked against current state: once the period has been invoiced (by override or by hand) it stops appearing, so a non-empty list always means real money not yet billed.

  • create_invoice

    Creates a draft invoice for a client with one or more line items. Auto-generates an invoice_number (next sequential via finance.invoicing.numbering_prefix setting), resolves the client PO via the cascade (manual override → first non-null on job_ids), and computes header totals (subtotal/gst/total) from the supplied lines. Default GST treatment per line is 'taxable' — Australian GST-inclusive: gst = round(amount_cents / 11). Emits invoice.created with PO + totals in payload (idempotency key invoice.created:<id>).

  • update_invoice

    Updates the header fields of a draft invoice (subject_line, notes, client_po_number, due_date, issued_at). Issued invoices are immutable — attempting to edit a sent/overdue/paid/voided invoice returns code:'immutable'. Optimistic-concurrency-gated on expected_lock_version: stale values return code:'stale_version' with the current lock_version so the caller can refresh and retry.

  • void_invoice

    Voids an invoice in draft/sent/overdue/disputed status. REJECTS attempts to void a paid invoice — the correction path for paid is the refund flow (FE-05) + a credit note. Requires a non-empty void_reason and an explicit void_date (Principle 3 — no default to today). Increments lock_version. Emits invoice.voided with the reason in payload.

  • add_invoice_line

    Adds a line item to a draft invoice. Recomputes header totals (subtotal/gst_amount/total) atomically from non-deleted lines. Increments lock_version. Issued invoices are immutable — non-draft attempts return code:'immutable'.

  • update_invoice_line

    Updates an existing line on a draft invoice. Recomputes line GST + header totals atomically. Issued invoices are immutable.

  • remove_invoice_line

    Soft-removes a line from a draft invoice (sets deleted_at). Recomputes header totals. Issued invoices are immutable.

  • validate_invoice

    Runs the locale-specific tax-invoice validator against a draft or sent invoice and returns the structured result (ok + code + reason + carry-through fields). v1 ships the AU ruleset (lock-in B5 + WP-FE-06): supplier ABN required, recipient ABN required when GST > 0 AND total ≥ $1,000 (tenant-configurable threshold), GST sum sanity, all lines have explicit gst_treatment, tax-invoice-label correlate. The jurisdiction defaults to tenant setting `finance.tax.locale` ('AU' fallback). Future jurisdictions plug into the dispatch table without call-site changes.

  • recalculate_invoice_gst

    Recomputes per-line and header GST on a draft invoice from current `gst_treatment` flags. Bumps `lock_version`. Emits `invoice.gst_recalculated`. Lines with `gst_treatment='mixed'` PRESERVE their stored `gst_amount` (the operator-set value) — recalc only rewrites `taxable` (1/11 of inclusive amount) and `gst_free` (zero).

  • propose_resend_invoice

    Stages a re-delivery of an already-sent invoice to all finance contacts (or an explicit override). The invoice must be in `sent`, `overdue`, or `partially_paid` status. Returns a pending_operation_id; the email fires on `confirm_pending_operation` (two-leg propose → confirm — no separate approve, the invoice was operator-approved at original send). Does NOT mutate the invoice (no number/total/status/lock_version change), runs no financial postings, emits no invoice.sent — it re-delivers the existing invoice. Recipients fan out to every site contact flagged receives_invoices (de-duped), falling back to the client billing_email; pass recipient_emails to override with a specific set.

  • propose_record_payment

    Stages a client payment recording for operator approval. Validates date discipline, amount, payment method, invoice state, and external-reference idempotency BEFORE staging. If the gate would block, returns the specific code so the operator can fix first.

  • approve_record_payment

    Approves a staged payment recording and atomically inserts the payment row, allocation, transitions the invoice (to partially_paid or paid), and emits payment.recorded + (conditional) invoice.partially_paid or invoice.paid.

  • void_payment

    Soft-deletes a payment and rolls back the linked invoice's status. Required void_reason + void_date for audit-trail discipline. Use for typo recovery (wrong amount, wrong invoice) or bank reversal (deposit clawed back).

  • get_invoice_full_history

    Returns the complete story of a single invoice: header (with PO + lock_version + status + email_send_status) + state transitions from audit_log + line items + every allocated payment + every refund against those payments + email send_history from email_deliveries + linked contractor payables. Money in integer cents.

  • manual_resync_invoice_to_xero

    Manually mirrors a biloh invoice to Xero as a draft. Recovery path for invoices that failed to mirror automatically (status='failed' in xero_mirror_log). Idempotent — by default returns 'already_synced' if a synced row exists.

  • preview_late_fees_for_invoice

    Dry-runs the late-fee application for an invoice and returns what WOULD happen — same response shape as apply_late_fees_for_invoice but with zero side effects. Useful for previewing the daily-sweep behaviour or confirming a fee amount before pulling the trigger.

  • apply_late_fees_for_invoice

    Applies the owed late fee to an overdue invoice per the resolved config (use get_late_fee_config_for_client to inspect). Creates a NEW draft invoice anchored to the original via late_fee_for_invoice_id. Idempotent per (invoice_id, period_number) — a second call returns already_applied_this_period.

  • list_late_fee_invoices

    Returns every late-fee invoice anchored to a given original invoice, ordered by period_number ASC. Empty list = no late fees have been applied yet to that invoice.

  • apply_credits_to_invoice

    Manually triggers credit application against an invoice. Applies available client credits (prepayment, overpayment) oldest-first, creating credit_applications rows that reduce the invoice's remaining balance. Idempotent — safe to call multiple times; re-reads current balance and only allocates the gap.

  • create_payment

    Records a payment received against an invoice. `amount_cents` is INTEGER CENTS. Payment does not auto-update invoice status — that must be done separately if the invoice is fully paid. `payment_date` is a real-world financial event date and must be explicit (no NOW() default per Finance Engine Principle 3).

  • get_invoice_run_stocktake

    ONE read that answers 'what is sitting in drafts, and is it ready to send?' for every draft invoice in the tenant — the briefing that belongs BEFORE an invoice run. Per draft it returns the client, the amount (subtotal/GST/total in integer cents), how many days it has sat, a plain due phrase, the work being billed (service + site + service date per line, plus the job ids), the RESOLVED recipient list exactly as the send leg fans out, the greeting the send would open with and which rung chose it, anyone else who was eligible, whether the invoice can be addressed to a real person at all, the ATO tax-invoice validation result, and any duplicate-invoice warnings. `summary.by_reason` is the part to read first — it is the 'eight ready, two need a name' answer in one field.

  • stage_invoice_run

    Review a whole invoice run, then stage it in one call. Name the invoices you intend to send and get ONE table back showing, per invoice, the client, the total in integer cents, the resolved recipient list, the greeting the send would actually open with (including any greeting you override here), the ATO tax-invoice verdict and any duplicate-invoice warnings — plus `staged_ok` and `refusal_reason` on EVERY row, so a run that cannot go out in full tells you exactly which ones and why instead of returning a shorter table.

  • approve_invoice_run

    Approve and send a run of invoices you have already staged and reviewed — leg 3 for a whole run, in one call. Name the staged operations (`operation_ids`, from `stage_invoice_run`) and get ONE table back with, per operation, the invoice number, the recipients it actually went to, `sent_at`, `message_id` and `send_ok` — plus `refusal_reason`, `refusal_code` and `refusal_detail` on every row that did not go out, so a partial run tells you exactly which ones and why instead of returning a shorter table.

  • approve_send_invoice

    Approves a staged invoice send (leg 3 of 3 in the propose → confirm → approve send chain). Runs the locale-specific ATO validator (per WP-FE-06 + lock-in B5) as a HARD GATE: on validation failure the send is refused with a structured code (`missing_supplier_abn` / `missing_recipient_abn_above_threshold` / etc.) and `invoice.ato_validation_failed` is emitted. On validation pass, the invoice transitions to `sent`, the email is dispatched, and `invoice.sent` fires.

  • propose_send_invoice

    Proposes sending an invoice to the client. Creates an `mcp_pending_operations` row — the send is staged (stored status value 'pending') and MUST be confirmed via `confirm_pending_operation`, then approved via `approve_send_invoice`, before the invoice is actually sent. This three-leg pattern prevents accidental sends. Expiry is tenant-tunable via finance.sends.pending_operation_ttl_minutes (default 60, clamp 5-240).

Operations (9)
  • list_pending_detail_changes

    Lists every unresolved detail change across the tenant's clients and contractors: unreviewed profile changes (awaiting the operator's 'mark reviewed') and PENDING banking changes (held — payouts keep using the old account until approved). Each row carries the entity, the field-level old → new diff, the source surface, and any ABR verdict.

  • list_compliance_documents_pending_review

    Lists all compliance documents with status 'pending_review' for the tenant — the agent review queue. Results are oldest-first so the automation works FIFO.

  • cancel_pending_operation

    Cancels a pending operation in `pending` or `staged` status. After approve_send_operation has fired, the operation is `approved_and_sent` and cannot be cancelled — that's a separate withdrawal/voiding flow.

  • get_pending_operation

    Read-only inspection of a staged operation. Returns the operation's current state, a live render of the email that will go out, the attachment path, the signing_token, and the related entity details. The canonical "preview before approve" tool. For a pending send_credit_note it also returns `financial_preview` — what approving will take off the invoice, what overflows to the client's account, and the invoice's status afterwards (tool upgrade 5b24fdee).

  • list_pending_operations

    List staged or in-flight operations for the current tenant — the operator's approval queue. Default returns the most recent 20 operations that are GENUINELY WAITING: status 'staged' and not yet past their TTL. Every row names the artifact it is about — `entity_type` (e.g. 'invoice', 'payable'), `entity_id`, `entity_number` (e.g. 'INV-26-0149'), `client_name` (the operator-facing name, same semantics as list_invoices), `total_cents`, and `recipient_emails` (the resolved fan-out array, never a nullable scalar) — plus `expires_at`, `is_expired` and a prose `age_phrase` ('staged · 31 days ago · expired') so you never do the date arithmetic yourself. `related_entity_summary` reads 'send_invoice:INV-26-0149 · Byrnes Realty · $66.00'. MONEY OPERATIONS ARE HERE TOO (tool upgrade d1af996d): staged payments, refunds and payable releases awaiting approval are returned alongside sends, and carry a `money` block — `amount_cents`, `occurred_on` with an `occurred_verb` ('received' / 'refunded' / 'paid'), `method`, `reason` (a refund's required, non-empty audit trail), `external_reference`, `payment_id`, `invoice_id`, `notes` — plus the same `client_name` / `entity_number` / `related_entity_summary` a send row carries. A refund row's entity is the invoice it refunds; a payable-release row's entity is the PAYABLE (entity_type 'payable'), with its linked invoice on `money.invoice_id` and its number on `entity_number`. `money` is null on a send row. Filter by status ('staged', 'expired', 'approved_and_sent', 'cancelled') or entity_type ('proposal', 'invoice', 'statement', 'credit_note', 'work_order', 'contractor_agreement', 'quote_request', 'payment', 'refund', 'payable'). status:'staged' means GENUINELY WAITING across BOTH lifecycles — sends stage at status 'staged', money operations at 'pending' — so one call answers 'what is waiting on me'. status:'expired' returns the lapsed rows the default view now hides — a staged operation past its TTL is not work, and presenting it as work is what made an eleven-row queue unreadable.

  • list_expired_pending_operations

    List pending operations that lapsed past their TTL without being confirmed or approved — i.e. staged sends that silently died. Returns swept rows (status='expired') plus pending rows already past expires_at, newest first, with how long ago each expired. Default window: last 7 days.

  • update_pending_send

    Personalize the email of a staged send operation BEFORE it goes out. Slot-based editing: `greeting` (the opening salutation), `cover_message` (the personal note below it), and `subject`. Re-renders and returns the fresh email_preview — what you see is byte-for-byte what will send. Works on every send-family operation type (proposal, invoice, statement, credit note, work order, quote request, contractor agreement, and resends).

  • confirm_pending_operation

    Confirms a previously staged operation (leg 2 of 3 in the propose → confirm → approve send chain). Behaviour depends on operation_type:

  • approve_send_operation

    Approves a staged send operation and immediately sends the email to every staged recipient with a portal-link CTA (proposals fan out to the recipient_emails resolved and staged at propose time; the response's recipient_emails lists every address reached). Generic dispatcher — branches on `pending_operation.operation_type` to the right artifact-specific send path (proposal / invoice / contractor_agreement / work_order). The recipient lands in the portal, reviews, and signs / accepts / pays via the configured method.

Audit (2)
  • get_audit_log

    Returns recent audit log entries for the tenant. Supports filtering by actor, entity, action, and date range (time-window via `from_date` / `to_date` ISO params). The canonical audit timeline across the whole tenant.

  • get_audit_log_for_entity

    Returns all audit log entries for a specific entity in chronological order. The complete timeline for one entity — use for self-diagnosis of unexpected state. Pass include_domain_events:true to ALSO return the domain_events emitted for the entity (e.g. job.rescheduled, job.spawned, work_order.accepted) as { event_type, occurred_at, payload_summary } — the per-entity event read that confirms a documented event fired for a non-finance entity.

Platform (4)
  • report_bug_to_biloh

    Report, file, submit, log, raise, or flag a bug, defect, or issue with the Biloh platform — the CANONICAL WRITE TOOL for filing a bug report. When your intent is 'report a bug' / 'file a bug', call THIS tool: list_my_reported_issues is only the read-back of what you already filed, and list_platform_bugs is the platform-admin triage view; neither files anything. Bugs are stored at the platform level (cross-tenant, visible to platform admins). Downstream automation may pick up the bug and ship a fix; the calling agent is not notified of resolution from within this tool. INCLUDE A `suggested_fix` ONLY IF YOU'RE CONFIDENT — it's a hypothesis for the receiving agent, not a directive.

  • request_tool_to_biloh

    Request, propose, ask for, or submit a NEW tool to the Biloh platform — the canonical write tool for filing a tool request (list_my_reported_issues is the read-back of what you already filed; it does not file anything). Tool requests are stored at the platform level (cross-tenant). Downstream automation may design and ship the new tool; the calling agent is not notified of completion from within this tool. BE PRECISE about `proposed_name`, `tool_class`, and `workflow_context` — the receiving agent uses these to decide whether to build.

  • upgrade_tool_to_biloh

    Suggest, propose, file, submit, or log an upgrade or improvement to an EXISTING Biloh tool that works correctly but could communicate or structure itself better — the canonical write tool for filing a tool-upgrade suggestion. Upgrades are stored at the platform level (cross-tenant). Downstream automation may pick up the upgrade and ship it; the calling agent is not notified of resolution from within this tool.

  • render_artifact_pdf

    Renders a tenant artefact (proposal, signed agreement, work order, ongoing-series work order, invoice, quote, statement, credit note, contractor SMA, receipt) as a PDF, stores it in Supabase storage scoped to the tenant, and returns a 1-hour signed download URL. Idempotent — re-call to refresh after data changes. Updates the entity row's `pdf_url` / `signed_pdf_url` field if applicable.

Other (278)
  • lookup_abn

    Looks up an Australian business via the Australian Business Register (ABR). Supports two modes: (1) ABN lookup — provide `abn` to get verified details for a known ABN; (2) Name search — provide `name` (and optionally `state`) to find businesses by name, returns up to 5 matches. Works for businesses, body corporates, sole traders, trusts, and partnerships.

  • get_entity_detail_changes

    Returns the field-level before/after change history for a client or contractor — every self-service detail update (proposal acceptance, client portal, contractor portal) with old → new values, the surface it came from, ABR verification verdicts on ABN changes, and pending banking changes awaiting approval. This is the SAME data the operator's 'View changes' popup shows.

  • review_entity_details

    Marks a client's or contractor's self-service detail changes as reviewed — the same action as the operator's 'Mark reviewed' button. Stamps every unreviewed PROFILE change row, clears the review banner, and writes the audit trail (event + audit_log).

  • approve_banking_change

    Approves a contractor's PENDING bank-details change and applies the new account to all future payouts. HUMAN IN THE LOOP IS MANDATORY (architect policy D4): the operator must have verified the change with the contractor out-of-band — normally a phone call — before this runs. Payables keep paying the OLD account until this succeeds.

  • reject_banking_change

    Rejects a contractor's PENDING bank-details change — the contractor's current (old) bank details stay in place for all payouts. Use after the operator decides the change shouldn't apply: couldn't verify with the contractor, the contractor denies making it (possible compromise — also consider rotating their portal link), or it was submitted in error.

  • propose_send_quote_request

    Stages a quote request to one or SEVERAL contractors in one go: creates a `requested` quote row per contractor (visible on their portal quoting pages at once, all sharing one request_group_id) and ONE pending operation covering every email — the operator approves once. Each email's CTA deep-links that contractor straight to their quoting page. Two-step chain: this call sends NOTHING.

  • create_quote_upload_link

    Generates a short-lived, single-use upload link for photos/documents on a quote REQUEST. Give the link to the operator — they tap it on their phone, pick the photos, and every contractor in the request group sees them on their portal quoting page.

  • approve_send_quote_request

    Approves and SENDS a staged quote request — every recipient in the staging, one approval. Each contractor receives a branded email whose CTA deep-links to THEIR portal quoting page, pre-focused on their copy of the request, with the operator's scope of work pre-filling their quote form.

  • record_compliance_review

    Records a verdict on a compliance document. Verdicts: verified (document valid), accepted_with_exception (insurance only — valid but with noted exceptions), rejected, needs_more_info.

  • record_exception_confirmation

    Records that the CONTRACTOR (or their broker/insurer) has confirmed an exception you recorded — the counterparty's answer, as distinct from your own judgement.

  • create_compliance_upload_link

    Generates a short-lived, single-use upload link for a contractor's compliance document (insurance certificate, photo, scan). Give this link to the user — they tap it on their phone, pick a photo/scan/PDF, and the browser uploads it natively. This avoids the base64-through-chat corruption that affects large files.

  • generate_compliance_report

    Generates a point-in-time CONTRACTOR compliance report (insurer pack) — the currency of the tenant's own contractors' public liability, licences and engagement agreements. Returns the structured payload covering: engagement summary, compliance register, exceptions, gap list, process evidence, and minimums disclosure (including site floors). Each generation writes one compliance_report_snapshots row.

  • list_credential_types

    Lists the platform credential vocabulary — every credential a service can require and a contractor can hold (public liability, workers comp, electrical/gas/plumbing licences, white card, working at heights, ABN registration, …), with its `code`, human `label`, `category` (insurance|licence|card|registration), whether it `expires`, and a `verification_hint` for the reviewer.

  • list_frequencies

    Returns the frequency catalog (scheduling patterns: weekly, fortnightly, 4-weekly, etc.) for the authenticated user's tenant. Frequencies are referenced by contract_service_lines to determine job recurrence.

  • backfill_line_facts_from_description

    Seeds a contract service line's MISSING facts from the free-text description on its accepted proposal line — the repair for a migrated line whose structured facts were never captured (e.g. a null contractor_method_notes that would dispatch a contractor with no instructions). Reads the accepted proposal line's description and proposes writing it into the null fact fields (client_line_description, contractor_method_notes) for review. `confirm:false` (default) PREVIEWS the proposed mapping and writes NOTHING. `confirm:true` writes ONLY the currently-null facts (a populated fact is never overwritten) and appends a provenance note to the line's internal notes. It NEVER invents content: if the accepted proposal line has no free-text description, it refuses with a named reason instead of guessing.

  • set_line_attribute

    Creates or updates a custom key/value attribute on a contract service line. Attributes are the long-tail complement to first-class line facts — use them for trade-specific metadata (circuit IDs, bin sizes, unit numbers) that vary per line. Each attribute carries audience + render_map for structural visibility control.

  • remove_line_attribute

    Soft-deletes a custom key/value attribute from a contract service line. The attribute is marked with deleted_at but not hard-deleted — it can be re-created with set_line_attribute.

  • record_completion

    Records contractor completion of a job: sets `status='completed'`, `completion_submitted_at`, `completion_photos[]`, `completion_notes`. Validates min photos + notes per the tenant's contractor-portal completion requirements — these live in `tenants.contractor_portal_settings` and are NOT in the settings registry, so get_setting / list_settings will not find them (`min_completion_photos` default 0 — photo evidence is opt-in). DEADLINE MACHINERY (WP-MISSED-RECOVERY — teach the operator, don't let them discover it): the daily sweep flips dispatched jobs to 'missed' after `scheduling.missed.sweep_grace_days` clear days (registry, default 1); the contractor's portal Mark Complete stays available until scheduled_date + `scheduling.completion.late_window_days` (registry, default 7; per-contractor override `scheduling.completion.late_window_days_override`, -1 = inherit). A missed job is therefore NOT terminal: within the window the contractor self-completes from the portal; at any time the operator recovers it here with `backfill:true`.

  • record_field_triage

    Records a field-triage event on a dispatched job — the operator/MCP twin of the contractor portal 'Can't complete' flow. Sets the job's hold_reason from reason_code (+ optional note), auto-reschedules the occurrence (to reschedule_to, or the next available day — the day after the current date, never earlier than tomorrow), mints an 'auto_applied' schedule_change_requests row as the operator-facing record, and emits job.rescheduled + job.schedule_change_requested — i.e. exactly the state transition the portal triage produces.

  • import_bank_csv

    Ingest a CommBank NetBank CSV export into the bank-reconciliation feed (bank_transactions). The CSV is header-less with 4 columns: date (DD/MM/YYYY), signed amount (credit +, debit -), description, running balance. Ingest is IDEMPOTENT — re-importing an overlapping window adds zero duplicates, and two genuinely-distinct same-day/same-payer/same-amount credits (the Intum case) both survive.

  • list_bank_transactions

    Returns ingested bank transactions for the tenant, filtered by match_state (unmatched | suggested | matched | ignored), direction (credit | debit), and a value_date window (since/until ISO dates). Amounts are signed integer cents (positive = money in). By default hides is_test=true rows.

  • ignore_bank_transaction

    DEPRECATED — use triage_bank_transaction instead. Mark a bank transaction as a non-sale so it leaves the reconciliation worklist. Maps to triage_bank_transaction with category 'other' and preserves the reason as a note.

  • ignore_pre_cutover_transactions

    Cutover triage: bulk-mark every UNMATCHED bank line dated BEFORE a cutover date as ignored ('pre_biloh_history'), so the reconciliation worklist opens on only the deposits Biloh can actually settle. Use this once when a tenant migrated INTO Biloh mid-stream — their older bank lines reference invoices in their previous system and can never match here, so they would otherwise clog the worklist forever.

  • triage_bank_transaction

    Triage a bank transaction into a tenant-defined category (e.g. 'previous-system-payment', 'marketplace-sale', 'business-expense'). The line leaves the reconciliation worklist and appears in the Triaged tab with its category badge.

  • untriage_bank_transaction

    Reverse a triage decision: restore a bank transaction to the reconciliation worklist. The line goes back to match_state='unmatched' and all triage columns are cleared.

  • bulk_triage_bank_transactions

    Triage multiple bank transactions into the same category in one call. Useful for batch-clearing a set of similar lines (e.g. all old-CRM payments this month).

  • list_bank_triage_categories

    List all bank-feed triage categories for this tenant. Categories define what a non-invoice bank line was (e.g. 'Payments received in previous system', 'Marketplace sale', 'Business expense'). Each carries a platform rollup_class for cross-tenant reporting.

  • list_triaged_transactions

    List bank transactions that have been triaged (categorised as non-invoice). Optionally filter by date range and/or category key.

  • get_bank_triage_report

    Get a summary report of triaged bank transactions for a period, grouped by category or rollup class. Shows count, total credit cents, and total debit cents per group.

  • create_bank_account

    Registers one of the business's real bank accounts so the bank feed can tell them apart. A business typically runs several — an everyday operating account, a tax/BAS set-aside, a contractor buffer — and until each is registered, only one of them can be brought into the books.

  • list_bank_accounts

    Lists the tenant's registered bank accounts and, for each, where the feed has actually got to: current balance (the balance chain's closing figure, or the declared opening balance when nothing is imported yet), the last day the feed covers, how many lines it holds, and whether its chain is intact. This is the operator's 'what do I actually have, and is any of it stale?' read.

  • get_balance_sheet

    Read the BALANCE SHEET as at any date: what the business holds against what it owes, with owner's equity stated as the residual.

  • update_bank_account

    Corrects a registered bank account, or retires one. Archiving is is_active:false — the account disappears from pickers and from the default registry read, but its bank lines, balances and history stay exactly where they are and still count towards a period's completeness. Nothing here ever deletes.

  • create_bank_triage_category

    Create a new tenant-defined bank triage category — what a non-invoice bank line WAS (e.g. 'Fuel', 'Marketplace sale'). Each carries a platform `rollup_class` for cross-tenant reporting AND, since W4-4, a `reporting_category_id`: the P&L category it rolls into.

  • update_bank_triage_category

    Update a tenant-defined bank triage category. You can rename the label, change direction_scope, or adjust sort_order. The key and rollup_class are IMMUTABLE after creation — if the class is wrong, create a new category and archive this one.

  • archive_bank_triage_category

    Archive (soft-delete) or unarchive a bank triage category. Archived categories are hidden from pickers and block new triages, but all historical triaged rows remain joined and reportable. The 'other' category is protected and cannot be archived.

  • apply_bank_payer_rules

    Apply tenant payer rules to unmatched, untriaged bank transactions. Rules auto-triage lines whose description matches a saved substring, UNLESS the line's amount equals any outstanding invoice total (the One Percent guard).

  • triage_bank_feed

    Composite bank-feed triage tool. Two modes:

  • create_bank_payer_rule

    Create a payer rule that auto-triages bank transactions whose description contains the given substring into the specified category.

  • list_bank_payer_rules

    List all payer rules for the tenant. Payer rules auto-triage bank transactions whose description contains a saved substring.

  • deactivate_bank_payer_rule

    Deactivate (or reactivate) a payer rule. Deactivated rules stop firing on future imports but remain queryable for audit.

  • get_reconciliation_suggestions

    Ranks unmatched bank transactions against outstanding invoices (credits) and payables (debits), returning suggested matches with deterministic confidence scores. Credits: reasons include exact_amount, invoice_ref_in_description, alias_match/client_name_match, date_plausible; bands auto_suggest >= 0.9 AND no warnings, else needs_review. `reasons` holds EVIDENCE FOR the match only; cautions live in a separate `warnings` array (bug b6d85348) — never read a warning as a point in the match's favour. An invoice number printed in the bank text is the STRONGEST single signal (it outranks a bare amount coincidence, which says nothing about who paid) and surfaces the invoice on its own even when the amount differs — a split, a part payment or a fee deduction. Both the client's legal and trading names are matched, because banks print the account-holder's legal name. A candidate may carry `alreadyPaid: true` + warning `already_paid`: that invoice is ALREADY SETTLED and is only offered because the bank text names it (the payment was recorded before the line was reconciled — usually keyed by hand weeks before the CSV import). ALWAYS tell the operator a candidate is already paid before proposing it. Do not attempt the match: match_bank_credit_to_invoice REFUSES a settled invoice with code 'invoice_already_paid' and there is no override — the line wants LINKING to the payment that already exists, not a new one. THE NEXT STEP IS link_bank_credit_to_existing_payment (tool upgrade 3742bc31), and where the payment is identifiable this tool now names it for you: each suggestion carries `linkablePayments`, serialised AHEAD of `candidates`, listing every live un-reconciled payment whose OWN reference names this bank row, with {paymentId, amountCents, paymentDate, invoiceId, invoiceNumber, clientName, evidence, confidence, coversDepositFully}. `confidence: "certain"` is reserved for that evidence and is never reachable from amount or date resemblance — a human wrote the reference, which is why it outranks every scored candidate. Note the reference form is HAND-KEYED (`bank:<bank_transaction_id>`, optionally `#INV-...` where one deposit covered several invoices); the platform's own reconciliation stamps are `bankrec:` and `bankrec-link:`, and a payment carrying one of those is already reconciled and is never offered here. Each suggestion also carries `recommendedAction` — 'link_existing_payment' when linkablePayments is non-empty, else 'match_invoice' when there are candidates, else 'operator_decision' — so you are told which reconcile verb applies rather than inferring it from the shape of the response. Each suggestion also says WHAT THE LINE IS (tool upgrade f9ef67c3), independent of whether any candidate was found: `lineKind` (client_payment | stripe_settlement | merchant_settlement | internal_transfer | contractor_related | refund | unknown), `lineKindEvidence` (what drove it — e.g. `payer_rule:<substring>-><category_key>` from the tenant's own payer rules, `paired_debit_in_own_account` when the credit is half of a possible_transfers pair, `invoice_ref_in_description`, `amount_only_candidate`) and `lineKindConfidence` (certain | probable | guess). `unknown` means NO OPINION — it is not a client payment we failed to place (that is client_payment with no candidates). The label is driven only by tenant-editable payer rules and structural evidence, never by hard-coded bank descriptors, so it labels little until the tenant has rules; teach it with create_bank_payer_rule (a substring filed into a category whose rollup is internal_transfer or refund, or the contractor-payment category). It is a LABEL ONLY: candidates, bands and recommendedAction are unchanged by it — a line labelled internal_transfer can still carry an amount-coincidence invoice candidate, and you must not match an internal transfer to an invoice (it would overstate income). A deposit covering SEVERAL recorded payments is not expressible yet: the link path refuses a partial cover, so only a one-to-one link is ever recommended. The warning `amount_mismatch` means the deposit is not the invoice's total (a part payment, a split, a fee deduction): a real suggestion, but one a human decides. Any warning caps the band to needs_review, so a warned candidate is never eligible for bulk confirm. One deposit covering SEVERAL invoices (a split) is not suggested as a set; when the bank text names several invoices of one client, reconcile it with match_bank_credit_to_invoices (explicit amount per invoice, adding up to the deposit exactly). Debits: reasons include exact_amount, contractor_alias, date_proximity — and (WP-CONTRACTOR-BILLS) every debit suggestion now carries `billReference` (parsed 'Inv # N' from the description), `billCandidates` (open contractor_bills with outstanding_cents; a reference match scores 0.95 `bill_reference_match` — settle via settle_bill_with_bank_debit), payable candidates enriched with `createdAt` + `jobContext` {siteName, serviceName, jobDate} + `linkedInvoice`, and `excludedImpossible` — the count of same-amount payables HARD-EXCLUDED because they were created after the money left (they cannot be what the debit paid). Returns `suggestions` (credits), `debit_suggestions` (debits) and — WP-W4-2 — `possible_transfers`: pairs where a debit in one registered account and a credit in ANOTHER are equal in magnitude and dated within finance.bankfeed.transfer_window_days (default 3) of each other. Those are the business moving its OWN money (the weekly sweep to the tax set-aside, a contractor-buffer top-up) — confirm one with match_transfer_pair and both halves stop being income to one report and an expense to another. Each entry carries both bank_transaction_ids, both account names, both value dates, the amount in integer cents and the day gap. Finally — tool request f1c24c84 — `ignored_debit_candidates`: bank debits the operator filed as IGNORED that nevertheless reconcile against an open payable or bill. These are money that left the account and was never recorded, and they are invisible to `debit_suggestions` by construction (it reads only match_state='unmatched'), which is how a paid bill can age for months as still-owing. They are returned as a SEPARATE array and are NEVER eligible for bulk confirm: an ignored line cannot be matched directly, because match_bank_debit_to_payable requires match_state='unmatched'. Each entry carries `matchState: 'ignored'` and `suggestedAction` naming the two-step (untriage_bank_transaction, then match). Most ignored lines are correctly ignored — treat this list as 'worth a look', not as a worklist, and always show the operator the line before proposing to un-ignore it. The reconcile screen's own worklist is unaffected.

  • get_stripe_payout_breakdown

    Explains a Stripe settlement deposit in the bank feed: which card payments it contains, what Stripe kept in fees, any refunds netted out, and whether it adds up to the cent. A Stripe payout arrives as ONE net credit covering many charges, so it never equals any single invoice and get_reconciliation_suggestions has no candidate for it — this is the tool for that line. It resolves the payout (from `payout_id`, else a po_ id printed in the bank description, else the one payout on the tenant's connected Stripe account whose amount equals the deposit exactly and which arrived within finance.bankfeed.date_window_days before the bank date), lists every constituent charge with the biloh payment it already produced (matched on payments.reference = the payment_intent id), and returns grossCents, feeCents, refundCents, otherCents, netCents, depositCents and gapCents = deposit − net, SIGNED and to the cent. `reconciles` is true only when the gap is exactly zero AND every charge maps to a biloh payment that is not already reconciled. Each constituent carries `issue`: null, 'no_payment_found' (Stripe took a card payment biloh never recorded — named, never invented), 'payment_already_reconciled', or 'no_payment_intent'. Refunds are carried separately and are never counted as fees. A gap is REPORTED, never rounded or absorbed into the fee figure.

  • settle_stripe_payout

    Settles a Stripe payout deposit in the bank feed in ONE call: links the ONE bank credit to EVERY card payment the payout carried, books the fee Stripe kept as an expense, and marks the line matched. Two-phase — confirm:false PREVIEWS (writes nothing, `writes: "none"`); confirm:true EXECUTES all-or-nothing.

  • unlink_document_from_entity

    Unlink a document (receipt, tax invoice) from the ONE entity it was wrongly attached to — e.g. a receipt linked to the wrong bank line — without deleting the document. Detach / remove a wrong document link.

  • match_transfer_pair

    Marks a debit in one of the business's bank accounts and a credit in another as the two halves of ONE movement of its own money — the weekly sweep to the tax set-aside, a top-up of the contractor buffer. Both lines move to match_state 'transfer': income to no report, expense to no report, GST to nothing — while staying real lines in both accounts' balance chains, so no balance moves by a cent.

  • unmatch_transfer_pair

    Undoes a confirmed transfer pair: the two bank lines stop being 'this is my own money moving' and go back into the money-in and money-out worklists. Each line returns to the state it actually held before it was paired (recorded at pairing time), not a guessed default. A reason is REQUIRED — this is the operation that puts two lines back into the reports, and the audit trail has to say why. Nothing is deleted: the pair row survives, marked 'unpaired', as the record of what was once paired.

  • list_reporting_categories

    Lists this tenant's chart of reporting categories — the categories a P&L is built from — each with the `pnl_treatment` that says what it DOES to the bottom line: `income` (money earned), `expense` (money spent earning it), `excluded` (money that moved but belongs to neither — the owner's own drawings), or `liability_clearing` (a payment that settles a liability the platform already tracks, e.g. a BAS remittance, which is neither an expense nor a transfer).

  • create_reporting_category

    Adds a reporting category to this tenant's chart. `pnl_treatment` is REQUIRED and is the only thing a report will ever consult about it: `income`, `expense`, `excluded` (money that moved but belongs in no P&L line — drawings), or `liability_clearing` (settles a liability the platform tracks, e.g. an ATO/BAS remittance).

  • update_reporting_category

    Renames, re-treats, reorders or archives one reporting category.

  • set_triage_category_mapping

    Points a bank triage category at the reporting category it rolls into — the mapping that gives a coded bank line a meaning in the P&L.

  • list_vendors

    Lists this tenant's vendor registry — who the business pays, and the defaults their money-out coding starts from: `default_reporting_category_id` (the P&L home) and `default_gst_treatment` (`gst` | `gst_free` | `mixed`).

  • create_vendor

    Registers a supplier the business pays, with the defaults its money-out coding starts from.

  • update_vendor

    Edits one vendor's details or defaults, or archives it with `is_active: false`.

  • verify_vendor_abn

    Verifies a vendor's ABN against the Australian Business Register and records what the register says — including its GST answer, with provenance `abr_verified`.

  • create_vendor_coding_rule

    Creates a deterministic money-out coding rule: bank lines whose description CONTAINS the given substring (case-insensitively) are matched to a vendor, and a coding is STAGED from that vendor's defaults.

  • list_vendor_coding_rules

    Lists this tenant's money-out coding rules IN THE ORDER THEY FIRE — lower `priority` first, ties broken by creation time. Reading them in firing order is the point: two rules that both match a line resolve by this order, so a list sorted any other way would misrepresent what the engine will do.

  • deactivate_vendor_coding_rule

    Deactivates one money-out coding rule so it matches nothing from now on.

  • apply_vendor_coding_rules

    Runs the money-out coding rules over unmatched, untriaged DEBITS and STAGES a coding for each match.

  • list_staged_vendor_codings

    Lists the money-out codings STAGED and waiting for the operator — one card per bank line: the matched vendor, the rule that matched it, the reporting category, the GST treatment and the claimable cents.

  • stage_vendor_coding_from_document

    Codes a money-out bank line FROM WHAT THE INVOICE'S OWN LINES SAY, and OFFERS the result for the operator to confirm.

  • list_shoebox_items

    Lists the receipts waiting in the SHOEBOX — paper that landed BEFORE, or WITHOUT, any bank line.

  • suggest_shoebox_pairings

    OFFERS pairings between waiting shoebox receipts and unspoken-for bank debits. WRITES NOTHING.

  • confirm_shoebox_pairing

    The DECISION: attaches a waiting shoebox receipt to a bank debit as its primary evidence.

  • record_shoebox_cash_expense

    Exits a shoebox receipt as a CASH EXPENSE — a purchase that will never appear in any bank feed.

  • create_shoebox_upload_link

    Generates a short-lived, single-use link for snapping a receipt straight into the SHOEBOX from a phone.

  • move_document_to_shoebox

    Puts a document the platform ALREADY holds into the shoebox queue, so it can be paired with a bank line or exited as a cash expense.

  • confirm_vendor_coding

    Accepts a staged money-out coding, writing it onto the bank line: the vendor, the reporting category, and the claimable GST in integer cents. This is the ONLY door through which a staged coding becomes a figure any report can see.

  • get_category_totals

    Returns a period's money grouped by reporting category, in integer cents, with each category's `pnl_treatment` beside it — the read a cash P&L is built from.

  • list_transfer_pairs

    Lists the movements of the business's own money between its own bank accounts that have been confirmed as transfers — each with both accounts named, both value dates, the amount in integer cents, and (for an undone one) the unpair reason, actor and time. Because the pairing table is append-only, this is the whole history of the domain: pass status 'unpaired' to see what was undone and why, or 'all' for both.

  • get_sales_gst_report

    The accountant's month-end artifact: the CASH-BASIS Sales & GST report for a period. Returns one row per payment RECEIVED (invoice number, issue date, paid date, client, ex-GST cents, GST cents, total cents), the period totals, and the AU financial-year GST-period label (e.g. 'Q4 (Apr–Jun 2026)'). CASH BASIS means a sale and its GST are recognised when the money LANDS (payment_date in the period), NOT when the invoice was issued — so an invoice raised last month but paid this month appears in THIS month's report. GST per receipt = round(amount_cents / 11) at the AU 10% rate. Omitting the period defaults to the CURRENT calendar month.

  • get_month_end_status

    Returns the month-end CLOSE state for a period plus a LIVE summary. `state` is 'open' (no close exists yet), 'closed' (locked — figures frozen), or 'reopened' (was closed, now editable again). `live` carries the current received / ex-GST / GST / receipt_count / outstanding_ar / awaiting_reconciliation, computed fresh from the ledger. `close` (when present) carries the FROZEN snapshot taken at close plus closed_at and any reopen reason. Comparing live vs the frozen snapshot tells you whether the period drifted after it was closed. `bankFeeds` carries one entry per registered bank account — how far its feed reaches and whether that is current (state fresh | stale | never_imported), with `staleFeedCount` as the headline. It is ADVISORY: a close is gated on the balance CHAIN being intact, never on how recently a statement was imported. Omitting the period defaults to the CURRENT calendar month.

  • run_month_end_close

    Closes (locks) a month-end period. Computes the cash-basis figures for the period and SNAPSHOTS them (received / ex-GST / GST / receipt_count / outstanding_ar) into the period_closes record, marking the period 'closed' and stamping who/when. This is the sign-off step: the frozen snapshot is the official figure for the period even if later activity changes the live numbers. IDEMPOTENT — running it again on the same period updates the same single record (re-snapshots), never creating duplicates. The Financial Operations Test: this changes NO invoice/payment amount or GST value and executes NO payment — it records an attestation that the month is signed off. Omitting the period defaults to the CURRENT calendar month.

  • run_period_close

    Closes (locks) a period on the month → quarter → financial-year close ladder, snapshotting its cash-basis figures (received / ex-GST / GST / receipt_count / outstanding_ar) into period_closes and marking it 'closed'. Give `kind` (month|quarter|fy) and ANY date inside the period as `period_start`; the tool computes the canonical AU span and the kind-aware label for you (kind=quarter + 2026-05-15 → Q4 Apr–Jun 2026, label '2026-Q4'; kind=fy + any date in 1 Jul 2025–30 Jun 2026 → 'FY26'; kind=month + 2026-06-10 → '2026-06'). A month, its covering quarter, and the FY are DISTINCT closes and coexist. IDEMPOTENT per (kind, period) — re-running re-snapshots the same row.

  • get_period_ladder

    Returns the whole month → quarter → financial-year CLOSE LADDER for a financial year in one call — the agent's view of the Periods screen. For the FY containing `fy` (any ISO date inside it; defaults to today's FY): the 12 calendar months (Jul→Jun), the 4 AU-FY quarters (Q1 Jul–Sep … Q4 Apr–Jun), and the FY row, each with its close state (open | closed | reopened | stale) and, when closed, the frozen received + GST snapshot. `stale` means a child period was reopened after this period was closed — re-close it (run_period_close) to refresh the roll-up. READ-ONLY.

  • get_period_completeness

    Proves (or disproves) that the imported bank lines for a period are COMPLETE, using the bank's own running balance as the tie. Orders the period's bank lines by value date then import order and checks that each line's balance_after equals the previous line's balance_after plus this line's signed amount. If that chain holds from the first line to the last, NO LINE IS MISSING between them — the bank's arithmetic says so. If it breaks, the response names the first break: the span it sits in (previous_value_date → value_date), the expected and actual running balance, and the exact gap in cents. READ-ONLY — it changes nothing.

  • reopen_period

    Reopens a previously-closed month-end period so its figures can be corrected, then re-closed. Sets the period_closes record to 'reopened' and records a REQUIRED reason plus who/when — the reason is the audit trail for why a signed-off month was touched again. After reopening, make the correction (e.g. record a late payment, confirm a missed deposit) and call run_month_end_close again to re-freeze the snapshot. The Financial Operations Test: changes NO invoice/payment amount or GST value and executes NO payment — it flips a sign-off flag with an audited reason.

  • unmatch_reconciliation

    Undo a reconciliation: reverse a deposit that was matched to an invoice. It VOIDS the payment the match created (which unwinds the payment's allocation and recomputes the invoice off 'paid'), then frees the deposit back to 'unmatched' so it can be re-reconciled. A reason is REQUIRED (audit trail); nothing is hard-deleted. The Financial Operations Test: this reverses a previously-recorded payment via the audited void gateway — it changes no GST/amount computation and moves no money.

  • get_payable

    Returns a contractor payable by ID with its linked invoice (id+status) for Layer-2 cash-flow visibility. Amount in integer cents. `payable_paid_date` is the real-world payment date (Finance Engine Principle 3).

  • list_payables

    Returns contractor payables for the tenant, filtered by contractor_id / status / since. Amounts in integer cents. Default hides `is_test=true`.

  • get_credit

    Returns a client credit by ID. `source` distinguishes overpayment | prepayment | adjustment | refund_reversal | goodwill | legacy_migration (lock-in B2 — prepayments arrive before any invoice; goodwill and legacy_migration are originated by an operator via create_client_credit). Amount in integer cents. `origin_date` is the real-world date the credit arose (Principle 3) and is NULL on rows minted by biloh before that column existed — fall back to `created_at` when it is null.

  • list_credits

    Returns client credits for the tenant, filtered by client_id, source, since. Amounts in integer cents. Default hides `is_test=true`.

  • get_refund

    Returns a refund by ID with its linked payment summary (lock-in B8.1 linkage). `refund_date` is the real-world execution date; `refund_reason` is required non-empty. Amount in integer cents. Voided refunds ARE returned, flagged `voided:true` (deleted_at = the void timestamp), mirroring get_payment / get_invoice_full_history; a `Refund not found` error means the id is genuinely unknown, not voided.

  • list_refunds

    Returns refunds for the tenant, filtered by payment_id, since (refund_date). Amounts in integer cents. Default hides `is_test=true`.

  • get_statement

    Returns a statement by ID with its items (invoice + payment line refs). `bundle_invoices` flag indicates whether the eventual email attaches per-invoice PDFs. Amounts in integer cents.

  • list_statements

    Returns statements for the tenant, filtered by client_id and since (period_end). Amounts in integer cents. Default hides `is_test=true`.

  • get_finance_settings

    Returns the tenant's finance configuration from two sources: (1) registry-backed settings (category=finance) with current values, defaults, and tier status; (2) tenant-level finance columns (payment terms, GST rate, currency, overdue interest, payment instructions, bank details) and business profile identity (name, trading name, ABN, address, phone, email) PLUS the configured email sender (default_from_name, invoices_email, proposals_email, dispatch_email, reply_to_email) and a resolved effective_invoice_from = the {display_name, address} the invoice send path would actually use, including the noreply@<tenant-domain> fallback when invoices_email is unset, and effective_senders = the {purpose, display_name, address, reply_to} each of proposals / finance / dispatch sends from right now (the same read Settings → Email shows as "Currently sending as"; the sender's home is the settings registry email.* keys). Use update_tenant_finance_profile to write the tenant-level columns.

  • update_tenant_finance_profile

    Updates tenant-level finance columns and/or business profile identity. These fields live on the tenants table — EXCEPT the email sender fields (invoices_email, proposals_email, dispatch_email, reply_to_email, default_from_name), whose one home is the settings registry (email.invoices_from, email.proposals_from, email.dispatch_from, email.reply_to, email.default_from_name): this tool writes them through the registry, so get_setting and the Settings → Email screen show exactly what invoices are sent from. This is the MCP counterpart to the Financial Defaults + Business Profile UI pages.

  • get_payable_release_status

    Returns the Layer 2 cash flow protection gate state for a contractor payable: whether it is releasable and why/why-not, plus the linked client invoice's status, PO, and paid date.

  • propose_release_payable

    Stages a contractor payable release for operator approval. Validates that the Layer 2 gate (linked client invoice must be paid) is passable BEFORE staging — if blocked, returns the specific gate code so the operator knows what to fix first.

  • approve_release_payable

    Approves a staged contractor payable release and atomically transitions the payable to status='released'. Emits payable.released with idempotency key. Re-validates the Layer 2 gate (invoice still paid, payable still releasable) defensively.

  • void_payable

    Soft-deletes a non-released contractor payable (status → 'void', deleted_at set). Required void_reason + void_date for audit-trail discipline. The symmetric partner to void_payment for the contractor money-out side.

  • propose_record_refund

    Stages a client refund recording for operator approval. Refund anchors to a payment (lock-in B8.1) and requires a non-empty refund_reason (B8.2). Validates date discipline, amount, method, payment state, and refundable-balance BEFORE staging. If the gate would block, returns the specific code so the operator can fix first.

  • approve_record_refund

    Approves a staged refund recording and atomically inserts the refund row, recomputes the linked invoice's status (paid → partially_paid or sent/overdue depending on effective allocation), handles linked contractor payables (pending+approved flips back to pending; released emits payable_unwind_required signal), and emits refund.recorded + (conditional) invoice.partially_refunded or invoice.refunded + (conditional) payable_unwind_required.

  • void_refund

    Soft-deletes a refund and rolls the linked invoice's status forward. Required void_reason + void_date for audit-trail discipline. Use for typo recovery (wrong amount, wrong refund) or refund returned to tenant (customer changed mind, refund cheque bounced).

  • create_credit_note

    Creates a DRAFT credit note against an existing sent invoice. Credit notes anchor to invoices (not payments — refunds do that), reduce the invoice's effective settled amount on send, and carry the originating invoice's PO + ATO fields automatically.

  • update_credit_note_line

    Patches a credit-note line. Only valid while the credit note is in draft. Recomputes the line's GST + the header totals automatically.

  • approve_send_credit_note

    Approves a staged credit-note send. Runs the locale-specific ATO validator (per FE-06 reuse — same rules for credit notes as invoices) as a HARD GATE. On refusal returns a structured failure (ok:false + code) without sending. On pass: transitions credit_note to sent, adjusts the originating invoice's effective settled amount, optionally mints a client_credits row for overflow, dispatches the branded email, emits credit_note.sent + credit_note.applied_to_invoice.

  • propose_resend_credit_note

    Stages a re-delivery of an already-sent credit note to all receives_invoices contacts (credit notes are invoice-family) or an explicit override. The credit note must be in `sent` status. The email fires on `confirm_pending_operation` (two-leg propose → confirm). PURE re-delivery: no credit-note mutation, no status change, no re-application to the invoice (the original send already did that). Pass recipient_emails to override the fan-out.

  • get_credit_note

    Returns a credit note by ID with its lines + linked invoice projection (status, total, effective settled).

  • list_credit_notes

    Lists credit notes filtered by client / invoice / status / date range. Defaults to excluding test rows. Each row carries invoice_number and client_name so a list reads without a second lookup; the operator screens (Finance → Credit notes, and a client's page) read the same rows.

  • void_credit_note

    Soft-voids a credit note. If the credit note was previously sent, rolls the originating invoice forward (recomputes effective settled). Emits credit_note.voided + (when sent) invoice.credit_unwound.

  • settle_payable_externally

    Records that a contractor payable was paid outside the bank feed (personal account, cash, etc.). Requires a note explaining HOW the payment was made. Sets the payable to released with paid_via='external'.

  • reverse_payable_settlement

    Reverses an external or legacy_backfill settlement on a contractor payable. Sets the payable back to approved and clears all settlement fields.

  • attach_bill_document

    Attaches a bill document (photo of invoice, PDF scan) to a payable OR a contractor bill for ATO substantiation. Accepts the file as base64, validates and uploads to storage, sets the document path on the target (payables.bill_document_path or contractor_bills.document_path). Exactly one of payable_id / bill_id. Re-attach replaces the previous document (both old and new paths are audited).

  • create_bill_upload_link

    Generates a short-lived, single-use upload link for a bill document (invoice photo/PDF). Give this link to the operator — they tap it on their phone, pick the photo/scan, and upload it natively. This avoids base64 corruption for large files. Target a contractor_bill (bill_id — preferred for multi-line bills recorded via record_bill) OR a single payable (payable_id — the backfill door). Exactly one.

  • match_bank_debit_to_payable

    Matches a bank debit (money out) to a contractor payable — confirms the payment happened. Sets the payable to released with the bank's value_date as the paid date, marks the debit as matched, and teaches a contractor bank alias for future matching.

  • record_bill

    Records a contractor's multi-line bill (tax invoice) as a first-class contractor_bill with lines linked to open payables. THE front-of-house door for ongoing contractor invoices — one invoice covering N jobs becomes one bill with N lines. GST is stored AS PRINTED on the paper, never derived (D3). Totals must add up (subtotal + gst = total) and every line must equal its payable's accrued amount — a disagreeing claim is a dispute, not a silent overwrite.

  • get_bill

    Returns a contractor bill by ID with its lines, its full settlement timeline (including reversed rows), settled_cents and outstanding_cents. The follow-the-money view for one bill: claim → lines → payables → payments.

  • list_bills

    Lists contractor bills for the tenant, filtered by contractor_id / status / since. Each row carries totals, variance, status and settled_cents (live settlements only) so an AP worklist can render without N+1 get_bill calls. Default hides is_test rows.

  • settle_bill_with_bank_debit

    Settles a contractor bill with one bank debit — the money-out mirror of confirming an invoice payment. Fans the debit out into payable_settlements rows across the bill's payables (oldest accrual first), releases every payable whose settlements now cover it, marks the debit matched (matched_bill_id), learns a contractor bank alias, and sets the bill part_paid or paid. Part payments are first-class: a debit smaller than the outstanding leaves the bill part_paid with the split recorded per payable. The bank's value_date is the paid date (Principle 3).

  • record_paid_bill

    Records a contractor bill that has ALREADY BEEN PAID and settles it against the bank debit that paid it — in ONE call, atomically. The bill, its accrual and the settlement are written together or not at all: this tool can never leave an open payable behind for money that has already left the account. GST is stored AS PRINTED on the paper, never derived (D3), and lands on the payable so the cash-basis purchases GST report can claim it. The bank line's value_date is the paid date (Principle 3).

  • settle_bill_externally

    Settles a contractor bill (fully or partially) with a payment made OUTSIDE the bank feed — personal account, cash, another bank. Requires the real-world paid date and a note explaining HOW. Same fan-out semantics as settle_bill_with_bank_debit, method=external, no bank transaction touched.

  • reverse_bill_settlement

    Reverses a contractor-bill settlement: marks the settlement rows reversed (NEVER deletes them — the ledger is append-only evidence), restores affected payables to approved with paid fields cleared, frees the bank debit back to unmatched, and recomputes the bill status. Scope to one payment with bank_transaction_id, or omit it to reverse every live settlement on the bill.

  • void_bill

    Voids (soft-deletes) an UNSETTLED contractor bill and un-stamps its payables back to unbilled so they can be re-billed correctly. A bill with live settlements is refused — reverse them first (reverse_bill_settlement). The claim-document discipline: a recorded bill is never silently edited; it is voided with a reason and re-recorded.

  • unmatch_bank_debit

    Reverses a bank debit → payable match. Returns the payable to approved status and the bank debit to unmatched, so both can be re-reconciled.

  • list_documents_for_entity

    List the evidence documents linked to an entity — finance documents (invoices, receipts, statements) for a payable, contractor_bill or bank_transaction, and OPERATIONAL evidence (job photos, completion evidence, compliance certificates) for a job or contractor. Returns document metadata, provenance, link role, and a short-lived signed download_url per row (download_url_error explains any row whose stored object could not be signed).

  • get_document

    Get a source document by ID, including all entity links, a short-lived signed download URL, and `extraction` — the cached reading of this paper (supplier, ABN, totals, GST, with the confidence and the verbatim source quote behind every figure), or null when nobody has read it yet. The key is always present, so you never have to guess whether the answer was 'nothing' or 'not looked'.

  • list_document_annotations

    Reads the operator's markings on documents AS DATA — the BAS-time question that used to be unanswerable: 'every document I marked under 100% business this quarter'. Every filter is a stored column, so the answer comes from rows; no document is opened and no page is read.

  • get_document_annotation_history

    The whole story of one document's markings, in order: every annotation ever made on it (including ones later superseded), every annotated copy rendered from it, the machine-keyed event stream, and the narrative audit rows — joined into one read.

  • record_document_extraction

    Stores WHAT YOU READ off a document — supplier, ABN, invoice number, date, totals, GST, line items — as a typed row CACHED BY THE DOCUMENT'S sha256, so the same paper is read once and every later caller gets your reading instead of squinting at it again.

  • get_document_extraction

    What has already been READ off this document, if anything: the cached extraction row — supplier, ABN (with its register cross-check), invoice number, date, totals, GST, line items — together with the confidence and the VERBATIM source quote behind every stored figure.

  • update_expense_record

    Update an expense record — state GST, attach a receipt, re-categorise, and add notes. Re-evaluates substantiation status automatically.

  • list_expense_records

    List expense records for this tenant. Filter by status, category, date range, or missing receipt.

  • split_bank_transaction

    Splits ONE bank line into two or more typed splits, so a MIXED receipt can be coded honestly. A bank line carries exactly one category and one GST figure, which makes a $180 supermarket debit holding $12.50 of business supplies structurally un-codeable — the only answer used to be 'code it personal and lose the credit'. Each split gets its own category, its own GST figure, its own note and its own receipt, and becomes its own expense record. The parent line moves to match_state 'split': it is no longer pretending to be one thing. GST is claimed ONLY on the splits that legitimately carry it.

  • unsplit_bank_transaction

    Reverses a split_bank_transaction, completely. The parent bank line goes back to the match_state it ACTUALLY held before it was split (not a default), the child expense records are removed from every live read along with their receipt links, and a whole-line coding the split had superseded is restored exactly as it was. Both halves of the story stay in the audit trail.

  • get_purchases_gst_report

    The cash-basis Purchases & GST report — the money-OUT mirror of get_sales_gst_report. Returns one row per contractor bill PAID in the period (bill number, bill date, paid date, contractor, ex-GST cents, GST cents, total cents), period totals, the AU GST period label, and footnotes (unpaid bills excluded from cash basis, out-of-gate + external settlement counts).

  • get_gst_position

    Net GST position for a period: GST collected (1A from sales) minus GST paid (1B from purchases). Returns payable_to_ato (positive net) or refund_due (negative net). Pure composition of the sales and purchases reports — no third money path.

  • get_profit_and_loss

    The CASH-BASIS profit & loss for a period, over the tenant's reporting categories, with the prior period of the same kind beside every line.

  • get_bas_worksheet

    The quarter's figures mapped to the ACTUAL ATO BAS labels, in the order the form asks for them, so lodging in ATO Online services is copy-across, field by field.

  • list_obligations

    Read the tenant's COMPLIANCE CALENDAR — every lodgement and renewal it owes: BAS each quarter, TPAR each August, super guarantee, plus any rego or insurance renewal the operator has added.

  • mark_obligation_lodged

    Record that an obligation has been LODGED — and file the proof with it.

  • upsert_obligation

    Add or correct ONE obligation — the door for the renewals the platform cannot know about: vehicle rego, public liability and other insurance, licences, a council permit.

  • list_assets

    Read the tenant's FIXED-ASSET REGISTER — everything the business owns that depreciates, with what it cost, when it was acquired, the depreciation method and effective life recorded against it, and where it came from.

  • create_asset

    Record a FIXED ASSET on the register — minted from the bank debit that paid for it, from a document already in the evidence layer, or by hand.

  • record_asset_disposal

    Record that a fixed asset was DISPOSED of — sold, traded in, or scrapped — and record the balancing adjustment that follows.

  • get_depreciation_schedule

    Read the FINANCIAL YEAR'S DEPRECIATION SCHEDULE from the asset register: opening written-down value, additions, depreciation, disposals and closing written-down value, per asset, with totals.

  • get_bookkeeper_worklist

    The bookkeeper desk's ONE first read: is there anything new to work, and how much is waiting on each stream. Returns has_new_work (the skip-if-nothing-new guard — false means there is nothing the desk has not already shown you, and a scheduled fire should END), counts by stream (untriaged bank lines, unmatched credits/debits, missing evidence, unpaired shoebox items, obligations due, stale pending operations), the contradictions fold (a coded expense riding an ignored bank line — two records that cannot both be true), the no_gst_claimed fold (every purchase substantiated WITHOUT a tax invoice because no GST was claimed — its stated basis, who set it, and the one-call Undo; tell the operator about any an agent set), and the high-water marks newness was judged against.

  • get_accountant_summary

    The weekly accountant-grade summary in one read: envelope report (set-aside and buffer targets vs actuals, plus the sweep card), period status per bank account and the close ladder above it, the exceptions block (untriaged lines, unmatched credits/debits, missing evidence, unpaired shoebox items, stale feeds, open and overdue obligations, and the contradictions fold), and the cards awaiting the operator with their ages.

  • recompute_substantiation

    Re-derives the substantiation state of EVERY expense record and bank debit this tenant holds, from the current amounts, documents and thresholds.

  • get_envelope_targets

    Answers the operator's own question: how much money should be sitting in the tax/BAS set-aside and in the contractor buffer right now, how much is actually there, and the gap. The tax target is the net GST position for the open BAS quarter to date plus an income-tax provision (finance.envelopes.income_tax_provision_pct of net cash profit for the financial year to date — DEFAULT 0, and the response says provision_unset until the operator sets the percentage with their accountant; the platform never guesses a tax rate). The buffer target is open contractor payables plus the projected cost of scheduled visits over the next finance.envelopes.buffer_horizon_weeks, priced at each visit's effective contractor rate. Actuals are each account's CHAINED bank balance, never a stored figure; an envelope with no account registered for its purpose reports no_account_registered and still shows the target, so the gap is named rather than hidden.

  • get_tpar_report

    The Taxable Payments Annual Report (TPAR) for a financial year: what this business paid its subcontractors, one row per contractor — entity name, ABN, address on file, total paid GST-INCLUSIVE, the GST component, and how many payments made it up — plus grand totals. This is the report the ATO wants lodged by 28 August each year, and it is produced from the payment ledger rather than assembled by hand.

  • get_aged_payables

    Returns aged payables (AP aging) bucketed by days outstanding: current (0-30), 30-60, 60-90, 90+. Shows unpaid contractor bills by age, mirroring the aged-receivables pattern.

  • create_payable

    Manually creates a contractor payable row in 'pending' status. CATCH-UP and OPERATOR-OVERRIDE path — the canonical flow is the job.completed handler which auto-creates payables. The release-side gate (Layer 2 cash flow protection — payable cannot release until linked invoice is paid) still applies.

  • list_statement_send_history

    Lists statement send and void events for the tenant, sourced from audit_log. Filter by client_id and since (ISO date). Returns chronological actor + action + statement_id + metadata.

  • propose_send_statement

    Proposes sending a statement to a client for a period. Creates a draft `statements` row capturing opening/closing balances + in-period totals, then stages an `mcp_pending_operations` row. Awaits `confirm_pending_operation` (stages for operator approval) then `approve_send_statement` (sends the email).

  • propose_resend_statement

    Stages a re-delivery of an already-sent statement to all receives_statements contacts (or an explicit override). The statement must be in `sent` status. The email fires on `confirm_pending_operation` (two-leg propose → confirm). PURE re-delivery: no statement mutation, no status change. Recipients fan out to every receives_statements contact (de-duped), falling back to client billing_email; pass recipient_emails to override.

  • void_statement

    Soft-marks a statement voided. Operator-attested error recovery (sent the wrong period, wrong client, etc.). Required `void_reason` (non-empty) + `void_date` (ISO YYYY-MM-DD; no default, Principle 3). Increments lock_version, sets status=voided + voided_at, emits statement.voided.

  • get_period_reconciliation_view

    Returns the two-sided (AR + AP) basis-aware reconciliation view for a period. AR side: invoices broken by lifecycle state (sent/overdue/paid). AP side: payables broken by lifecycle state (pending/released/paid). NET position = AR total − AP total. Accounting basis (cash/accrual) selects the date anchor: cash = money-movement dates (payment_date / payable_paid_date); accrual = earned/incurred dates (issued_at / created_at). The basis flips BOTH sides identically. Also includes legacy fields (billed_cents, paid_cents, outstanding_cents, refunded_cents), corrections, unhealthy_flags, po_breakdown. Money in integer cents. REFUND BASIS: paid_cents (and ar.paid) are GROSS receipts here — paid_cents_is_refund_net is false — so net cash = paid_cents − refunded_cents and net_cents (AR − AP) is gross-of-refunds; subtract refunded_cents exactly ONCE for the C09 'received − refunded == net cash' figure (refunded_cents is informational disclosure, do not double-subtract).

  • find_orphaned_or_unusual_events

    Returns the operator-alert anomaly stream — thirteen signal types surfaced by the temporal query layer:

  • get_xero_connection_status

    Returns the Xero connector state for the caller's tenant: whether the integration is enabled (tenant setting), whether an active OAuth connection exists, and the most recent error if any.

  • get_xero_authorize_url

    Returns a one-shot Xero OAuth authorize URL for the caller's tenant. The agent surfaces the URL to the operator as a clickable link; only a browser can complete the OAuth handshake.

  • list_xero_mirror_log

    Returns Xero mirror attempts (synced, failed, skipped) for the caller's tenant, ordered newest-first.

  • get_xero_mirror_for_entity

    Returns the chronological Xero mirror history for a single invoice or payable, plus the current synced Xero entity ID if one exists.

  • manual_resync_payable_to_xero

    Manually mirrors a biloh contractor payable to Xero as a Bill draft. Recovery path for payables that failed to mirror automatically.

  • disconnect_xero

    Disconnects the tenant's Xero integration. Idempotent — calling on an already-disconnected tenant returns already_disconnected:true. Best-effort Xero-side revoke; local connection row is always flipped to 'disconnected'.

  • get_stripe_connection_status

    Returns the Stripe Connect Express state for the caller's tenant: whether the integration is enabled (tenant setting), whether an active connection exists, and the capability flags from Stripe (charges_enabled, payouts_enabled, details_submitted).

  • list_stripe_webhook_events

    Lists Stripe webhook events for THIS tenant's CONNECTED (Till-2 / Connect) Stripe account only, sorted newest-first — e.g. payment_intent.succeeded / checkout.session.completed for the tenant's own client-invoice payments. Filter by status, event_type, or date range.

  • start_stripe_connect_onboarding

    Returns a Stripe Connect OAuth URL the operator opens in a browser to connect their existing Stripe account. Uses Standard accounts via OAuth — the tenant connects their own Stripe account (with their own bank details, KYC, and dashboard).

  • generate_pay_now_link

    Creates a Stripe Checkout Session for an unpaid invoice on the tenant's connected Stripe Express account. Returns the Stripe-hosted URL the customer opens to pay by card. Payment is recorded automatically via the Stripe webhook on payment_intent.succeeded.

  • generate_pay_all_link

    Returns the durable client-portal "pay everything" link for a client, plus a breakdown of what is outstanding. Opening the link lands the client on their portal Finance page and immediately starts ONE Stripe Checkout paying every outstanding invoice (sent / overdue / partially_paid) at its REMAINING balance. The payment is split across the invoices automatically by the Stripe webhook; any excess lands as credit on account.

  • disconnect_stripe

    Disconnects the tenant from Stripe Connect. Marks the local connection as disconnected; does NOT delete the Stripe Express account on Stripe's side (tenant retains ownership and can re-connect at any time).

  • get_email_delivery_status

    Returns the email delivery status for an outbound communication: pending/sent/delivered/bounced/soft_bounced/complained, a PER-RECIPIENT roll-up, the chronological Resend webhook event timeline, and (when the entity is a supported FE-09 type) the originating row's email_send_status flags. Pass EITHER message_id (Resend provider id) OR (entity_type, entity_id).

  • list_recent_bounces

    Returns recent bounce and (optionally) complaint events for the tenant — one row per outbound email that failed. Includes recipient, bounce reason from Resend's payload, originating entity (invoice/statement/refund/payment_receipt), and the provider message id. Default time window: last 24 hours.

  • request_schedule_change

    Contractor-side request to reschedule a job. Gated by the `scheduling.contractor_portal.can_change_schedule` registry setting (no | request_only | yes), falling back to the legacy `tenants.scheduling_settings.contractor_can_change_schedule` JSONB only when the registry key is unset. Records a pending request; admin must approve. Never moves the job directly when the setting is `no` or `request_only`.

  • list_schedule_change_requests

    Lists schedule change requests (client portal, contractor portal, field triage, request_schedule_change) with status, requested/effective dates, client name, and linked job context. Default filter is status='pending' — the operator's working set of requests awaiting approve/decline. `submitted_by_type` tells you who asked: 'client' or 'contractor'. The returned schedule_change_request_id is the input to approve_schedule_change_request / decline_schedule_change_request. The same rows render for humans under Pending Operations → Schedule requests.

  • approve_schedule_change_request

    Operator approves a pending schedule change request: atomically moves the job to the requested date (or `override_date`), marks the request 'approved' with actioned_at/actioned_by, links the audit trail to the request id, and emits job.rescheduled + schedule_change_request.actioned. Accepts schedule_change_request_id, or job_id when exactly ONE pending request exists for the job (multiple pending requests fail closed with the candidate list). Approval of a CLIENT-submitted request counts as a client-initiated reschedule: it increments the CSL's client_reschedule_count and is blocked by max_reschedules_per_quarter (architect-ratified 2026-06-06, amended 2026-09-24: client-initiated means submitted by the client) — use reschedule_job with origin=admin to move the job anyway. A CONTRACTOR-submitted request (submitted_by_type 'contractor') does NOT spend the client's allowance and is not blocked by it; its `csl` comes back null. Legacy requests with no recorded submitter keep the client-initiated behaviour.

  • decline_schedule_change_request

    Operator declines a pending schedule change request: marks it 'declined' with the reason in actioned_note, sets actioned_at/actioned_by, and emits schedule_change_request.actioned. The job is NOT touched. Accepts schedule_change_request_id, or job_id when exactly ONE pending request exists for the job (multiple pending requests fail closed with the candidate list).

  • repair_duplicate_csl_visits

    Guarded clean-up sweep for DOUBLED recurring visits (bug 9df52270 — the pre-fix residue of bug 0e337d2b). Finds pairs of live visits sitting inside one cadence slot on the same recurring line, keeps the ASSIGNED visit, and cancels only the unassigned duplicate — through the single cancel write-path, so each removal records an actor, a reason, and the schedule exception that stops the nightly spawner minting it again. PREVIEWS BY DEFAULT: dry_run is true unless you pass false.

  • get_dispatcher_view

    Composite — returns the canonical ScheduleView for the operator dispatcher: jobs bucketed into overdue/unassigned/today/upcoming/completed/cancelled, contractors-on-leave list, and aggregate stats (total / assigned / unassigned / overdue / completed). The `overdue` bucket carries every non-terminal job dated before tenant-today plus every `missed` job — the operator's most urgent slice (WP-SCHED-GOLD-0). Date defaults and bucketing anchor to TENANT-TIMEZONE today (never UTC); the response echoes `tenant_today` + `tenant_timezone`. Each job includes lock_version for OCC mutations. Honours the same tenant scope as list_jobs and list_unassigned_jobs. For busy tenants / large date windows use `summary: true` (each bucketed job carries only {id, scheduled_date, status, site_id, site_name, contractor_id} plus stats) or `count_only: true` (per-bucket counts + stats, no rows) to stay under the response size limit.

  • regenerate_csl_schedule

    Permanently changes a contract service line's frequency (cadence) and/or recurrence inputs (day_of_week, week_of_month, anchor_date), then regenerates its future undispatched jobs. Two-phase: call with confirm=false for a preview of what would change, then confirm=true to apply. Alias: change_csl_schedule.

  • backfill_period

    Backfills a migrated client's ALREADY-PERFORMED occurrences for a past period, derived from the contract service line's OWN recurrence rule — the missing bridge between 'client onboarded mid-month' and 'this month invoiced through biloh'. The spawner materialises forward only; this tool owns the backward direction. Occurrences BEFORE the CSL's anchor derive correctly (the anchor is back-shifted in whole periods, phase preserved — a weekly-Wednesday line anchored 23 Jul still derives 1/8/15/22 Jul). `confirm:false` (default) PREVIEWS: exact dates, per-visit legacy rate, period totals ex/inc GST, dedup against existing jobs, and a plain-language billing expectation from the client's invoicing cadence (a period_final_consolidated client gets ONE consolidated invoice when the month's last job completes — produced by the EXISTING machinery, not re-implemented). `confirm:true` writes. Two modes: 'dispatch' (default) lands the jobs dispatched + assigned in the contractor's portal for real completion; 'complete' completes them immediately through the canonical completion core (real job.completed events → payable + invoice handlers fire identically to a portal completion; completed_at = the visit's own date). The backdated period bills at `backfill_client_rate_ex_gst_cents` (legacy pricing survives a migration price rise); the CSL's forward rates are NEVER touched.

  • settle_legacy_visit

    Settles an already-performed legacy visit end-to-end in TWO calls: creates the completed job(s) on the visit date(s) and drafts the invoice, so 'I did this on the Nth, set it up and invoice them' stops expanding to ~10 plumbing calls. A visit-shaped wrapper over backfill_period (mode complete): give it a `date` (or `dates`), not a window. `confirm:false` (default) PREVIEWS the whole chain — the jobs it would create, the contractor it resolved, the legacy rate, period totals ex/inc GST, and the invoice expectation from the client's cadence — writing NOTHING. `confirm:true` completes each visit through the canonical completion core (real job.completed events → FE-04B payable + FE-02B invoicing-cadence handlers fire exactly as a portal completion would), drafts the invoice(s) per the client's cadence (a period_final_consolidated client gets ONE consolidated draft with a line per visit, service_date = each visit's own date), and returns draft_invoice_ids ready for propose_send_invoice. The backdated visit bills at backfill_client_rate_ex_gst_cents (legacy pricing survives a forward price rise); the CSL's forward rate is never touched. Idempotent: re-running the same visit dates returns already_existed with no duplicate job or invoice.

  • amend_series

    Amends a recurring series' cadence (frequency, day of week, week of month, or anchor date) as ONE operation. Two-phase: confirm defaults to FALSE (a preview that writes nothing); pass confirm=true to apply.

  • assign_series

    Assigns a contractor to a WHOLE recurring series (a contract service line): sets the line's contractor so every future spawn inherits them, re-derives what that contractor is paid, and sweeps the series' existing 'scheduled' occurrences onto them in one call.

  • reassign_series

    COMPOSITE — hand a whole recurring series (a contract service line) to a DIFFERENT contractor in one call. Two-phase: `confirm` defaults to FALSE and returns a preview that writes NOTHING; pass confirm:true to execute.

  • approve_counter_offer

    Operator approves a contractor_to_operator counter-offer, accepting the proposed schedule change.

  • set_csl_constraint

    Create or update a commitment constraint on a contract service line (CSL). Constraints define the negotiation boundaries for schedule changes.

  • remove_csl_constraint

    Remove (soft-delete) a commitment constraint from a contract service line. This is the escape hatch — proves a CSL cannot be bricked by an un-removable lock.

  • list_csl_constraints

    Lists active commitment constraints for a contract service line (CSL).

  • create_run

    Create (or reuse) a RUN for a day — the ordered list of stops a contractor or the operator will work, in sequence. Day-granular: a run groups a day's jobs and orders them 1..n (this is NOT time-slotting — biloh never schedules by time of day). MANUAL ORDERING ONLY — this makes no optimizer/Google call and works with route optimization OFF (optimization is a separate paid bolt-on that lands later). Idempotent: if a run already exists for that (date, contractor) it is reused and any new job_ids are appended. Jobs with a site that has no coordinates are still added to the run and SURFACED under no_location (never dropped). USE WHEN: building the day's run sheet order by hand. Pass contractor_id for a contractor's run, omit it for the operator/self run. Returns the run_id + the ordered view (stops + no_location).

  • get_run

    Read a RUN by id: its ordered stops (stop_order 1..n, each with site name/address/coordinates) plus a separate no_location list of stops whose site has no coordinates yet (surfaced, never dropped). Day-granular sequence, not time slots. Contractor-safe: the payload carries no client identity or rates. USE WHEN: showing or checking a day's run order. Read-only.

  • list_runs

    List RUNS for this tenant (optionally filter by run_date and/or contractor_id), each with its status and stop count. USE WHEN: finding the run for a given day/contractor before reading or reordering it. Pass contractor_id to scope to one contractor's runs; omit run_date to list across days. Read-only.

  • update_run_stop_order

    Set the MANUAL stop order for a run. Pass ordered_job_ids = the run's exact current job set in the new sequence; stop_order is rewritten 1..n in that order. This is a human-set order — no optimizer/Google call (optimization is a separate paid bolt-on). USE WHEN: the operator reorders the run sheet by hand. The list must contain exactly the run's current jobs (no additions/removals/duplicates) — add or remove stops with create_run first. Returns the reordered view.

  • create_team_member

    Creates a person on the team roster. This is a team member — an employee, volunteer, officer, or any person on your own workforce — NOT a contractor. Team members are dispatched directly (no work order, no acceptance step), and by default completed team-member jobs do not generate a payable.

  • update_team_member

    Updates a team member's details. Supports optimistic concurrency via expected_lock_version. Returns new_lock_version so you can chain edits without re-fetching.

  • archive_team_member

    Soft-deletes a team member (sets deleted_at). Existing job assignments remain as historical references. The person no longer appears in active lists or the assignee picker.

  • get_team_member

    Returns a single team member by ID, including their linked membership summary if linked to a user account.

  • list_team_members

    Lists team members on the roster. By default returns only active, non-test members. Use status and include_test filters to broaden.

  • link_team_member_user

    Links a team member record to a user account (auth.users). This connects the person on the roster to their login, enabling future features like self-service. Each user can be linked to at most one team member per tenant.

  • unlink_team_member_user

    Removes the link between a team member and their user account. The team member row and the tenant_users row both remain — only the link is severed.

  • onboard_team_member

    Composite: creates a team member and optionally triggers an app invite. Use this when adding someone new to the roster — it combines create_team_member with the invite flow in one call.

  • list_my_reported_issues

    Read back the bugs, tool requests, and tool upgrades THIS tenant has filed via report_bug_to_biloh / request_tool_to_biloh / upgrade_tool_to_biloh, with their current triage status. Tenant-scoped — returns only your own reports, never the cross-tenant platform queue.

  • send_operator_report_email

    Send a report email from the platform to the CONFIGURED operator address. The recipient is read ONLY from the tenant setting `notifications.operator_report_email` — you cannot pass an arbitrary recipient (open-relay guard). The sender is the platform's own identity from the `platform.email.*` settings (Platform Settings → Email), and the sending substrate follows that domain: biloh.com.au sends over Migadu, gwcpropertyservices.com.au over Resend. A domain needing a substrate this environment lacks is refused rather than sent under another domain.

  • list_settings

    Lists platform settings registered for this tenant with current values, default values, tier status, and metadata. Filterable by category, scope, and full-text search across keywords + descriptions.

  • get_setting

    Reads one platform setting's current value + metadata for this tenant. Returns the registered definition, the default value, the `previous_value` (if any), `tier_status` (enabled or upgrade_required), and the last-change authorship.

  • update_setting

    Updates one platform setting's value for this tenant. Writes audit_log + emits `settings.updated` event. Reversible via revert_setting (architect persona). Supports optimistic-concurrency via `lock_version`.

  • list_setting_history

    Returns the chronological audit history for one platform setting: every `setting.changed` and `setting.reverted` event with actor, timestamp, before/after values, and reason. Limit defaults to 20, max 100.

  • configure_setting

    Composite (front-of-house). Set a platform setting to a new value. Audit-logged + reversible via revert_setting. Use this as the default for natural-language settings changes; use update_setting directly when you need to pass `lock_version` for optimistic-concurrency control.

  • list_modules

    List all feature modules with their enabled/disabled state for this tenant. Each module groups related tools and nav items. Modules can be toggled via set_module_enabled. Core modules cannot be disabled.

  • set_module_enabled

    Enable or disable a feature module for this tenant. Disabling hides the module's tools and nav items but does NOT delete any data — background engines (invoicing sweeps, job spawning, etc.) continue processing. Core modules cannot be disabled. Modules above the tenant's tier cannot be re-enabled (upgrade required). Grandfathered above-tier modules that are currently ON can be disabled, but re-enabling requires the higher-tier plan.

  • get_session_context

    ⚡ READ ME FIRST ⚡ — Returns the full operational context for this tenant: business identity, your persona + role, key conventions, current state snapshot, and pointers to deeper reference. Call this before any other tool when you first connect. This is mandatory grounding for acting intelligently in this tenant.

  • get_attention_digest

    The operator's attention queue, digest-sized: decisions awaiting (Decision Queue cards), staged sends awaiting approval, untriaged service requests, stale pending amendment proposals, recent loud schedule signals (sold visits needing a decision, spawn skips), and missed visits (missed_visits — past visits with no completion, each with evidence {has_contractor_bill, has_payable, has_completion_photos}, a suggested_action likely_done_confirm | likely_not_done_confirm that is NEVER applied for you, and the next_tool that settles it: record_completion backfill, cancel_job or reschedule_job). Each bucket carries a count + newest few items + the tool that pages/acts on it.

  • search_tools

    Find the right MCP tool by intent — returns a ranked, persona-scoped shortlist (name + one-line summary + category) so you do not have to scan the full catalogue or guess a name.

  • get_workflow_guide

    Returns a platform-authored workflow guide: canonical step order (with exact tool names you can call), enum decision tables (which value to pick when), known gotchas, and relevant settings knobs. Tenant-specific notes are merged when configured.

  • get_documentation_directive

    Returns the current knowledge-extraction directive, ICP profiles, and instructions for staging public documentation. USE WHEN: You are told to 'run the documentation extraction' or 'write docs for biloh.com.au/docs' or 'document this on the platform docs'. The directive is the single source of truth for the extraction workflow. PRECONDITIONS: None — read-only. PLATFORM-GLOBAL: the directive is identical for every tenant — this read does not consult tenant data. On a PLATFORM connector, pass target_tenant_id with ANY active tenant id; there is no 'right' tenant to go hunting for. SIDE EFFECTS: None.

  • list_marketing_content

    Lists the tenant's marketing-site content for one kind (category | industry | case_study | page | help_article | testimonial | site_config). Returns published rows by default; pass include_unpublished:true to see drafts.

  • upsert_marketing_content

    Creates or updates one marketing-site content row (kinds: category | industry | case_study | page | help_article | testimonial | site_config). Insert vs update is resolved by payload.id, else by the kind's slug within the tenant. site_config always updates the tenant's single row; its `content` object is MERGED key-by-key (existing keys survive).

  • set_marketing_content_published

    Publishes or unpublishes one marketing-site content row (kinds with a published flag: category | industry | case_study | page | testimonial).

  • create_marketing_upload_link

    Generates a short-lived, SINGLE-USE browser upload link for a website photo. Give the link to the user — they tap it on their phone, pick the photo, add a caption, and it lands in the website media library. Then use list_marketing_assets + attach_marketing_asset to place it on the site.

  • list_marketing_assets

    Lists the tenant's website photo library (tenant_media_assets), newest first. Each item carries public_url (what attach_marketing_asset places on the site), kind, alt_text, and upload time.

  • attach_marketing_asset

    Places a library photo onto the website by writing its public URL onto a marketing content row.

  • get_branding_kit

    Returns the tenant's branding kit: every asset slot (logo_full, logo_white, logo_icon, favicon, email_header, pdf_header, social_banner, letterhead) with its status, required format/dimensions, and public URL where uploaded — plus brand colors, fonts, and completion percentage. This kit feeds invoices/PDFs, the app icon, the style guide, and the public website.

  • create_branding_upload_link

    Generates a short-lived, SINGLE-USE browser upload link that lands a file directly in a branding-kit SLOT (replacing the current asset at its stable path). Because every surface reads the kit, one upload updates invoices, the app icon, the style guide, and the website together.

  • create_branding_vendor_link

    Mints a branding VENDOR PORTAL link for an external graphic designer: a token URL where they see every kit slot with its spec (format, dimensions, background, size cap), upload BOTH the finished production file AND the editable source master per slot (provider-agnostic — Illustrator, Figma, Affinity, whatever they use), track progress, and submit the kit for review. Multi-use within its lifetime, unlike the single-use create_branding_upload_link (which is for the OPERATOR's own quick phone upload into one slot).

  • list_branding_vendors

    Lists this tenant's branding vendor portal links: designer name/email, status (active/suspended/expired), expiry, last access, and the portal URL for active links. PINs are never returned.

  • revoke_branding_vendor_link

    Immediately revokes a branding vendor's portal access (status → suspended). The link stops working on the next request. Uploads already delivered stay in the kit.

  • commission_branding_kit

    COMPOSITE — the one-call way to commission a branding kit from an external designer. Ensures the tenant's kit slots exist (seeds the 8 canonical slots if missing), mints a vendor portal link, and returns the delivery checklist (every slot with format/dimension specs and current status) plus a ready-to-send email draft for the designer containing the link and the brief.

  • export_branding_kit

    Packages the tenant's ENTIRE branding kit into a zip — every finished production asset, every editable source master (AI/Figma/PSD), and a generated BRAND-GUIDE.md documenting colors, fonts, and the asset inventory — and returns a time-limited (1 hour) download link. The kit is the tenant's property: this is the no-lock-in export and the brand-pack product's deliverable.

  • stage_public_doc

    Stages a public documentation article for the biloh.com.au/docs corpus. The article enters a validation pipeline (docs:check + docs:verify + sanitisation guard) before being committed by a repo-capable session. Never publishes directly. USE WHEN: You have extracted knowledge from a conversation and want to publish it as a public doc. Call get_documentation_directive first for the authoring contract. PRECONDITIONS: Directive loaded, article written per CONTRIBUTING.md. SIDE EFFECTS: Creates a staged_public_docs row with status='staged'.

  • list_staged_public_docs

    Lists the public-docs staging queue — articles submitted via stage_public_doc that have not yet been committed to the repo. Returns status='staged' rows by default (what is waiting to land); pass status to see 'landed' or 'needs_attention' rows, or status:'all'. The body is NOT included — use get_staged_public_doc to read one article in full.

  • get_staged_public_doc

    Returns ONE staged public-docs article in full — including body_md, summary and tags — so you can verify exactly what landed in each field rather than assuming the call that staged it was well-formed. Address it by `id`, or by the natural key `section` + `slug`. When both a staged and a landed row exist for one section+slug, the STAGED row wins (it is the one blocking a re-stage); pass `id` to target a specific row.

  • park_staged_public_doc

    Parks a WAITING staged public-docs article so it will not be landed, recording why. The article is not deleted: it moves to status='needs_attention', drops out of the worker's pickup set, and stays fully readable via list_staged_public_docs({status:'needs_attention'}) and get_staged_public_doc.

  • search_platform_docs

    Search and list the PUBLIC documentation corpus at biloh.com.au/docs. With no arguments, returns the lean index of every published article (slug, section, title, description, category, audience, tags, updated) — one call in place of walking the docs folder. Pass `q` for a ranked intent search, `section` / `category` / `tag` to narrow, or `slug` + `full:true` to read one article's body.

  • get_my_subscription

    Returns the current tenant's subscription status, plan, trial countdown, billing details, and the active plan's catalog price — price_monthly_cents, price_annual_cents, currency (AUD), tax_inclusive (true per ADR-0013), read verbatim from the plan catalog so a tenant can verify displayed price == charged price. Tenant-scoped — each tenant sees only their own subscription. It also returns the cancel lifecycle — cancel_at_period_end (true once a cancel-at-period-end is scheduled; the subscription stays active until current_period_end) and canceled_at (the cancellation timestamp, null when not cancelling) — so a tenant agent can read back a pending cancellation via MCP, not only via the Stripe portal.

  • get_my_entitlements

    Returns the calling tenant's EFFECTIVE feature entitlements resolved from its active subscription plan: the per-plan feature flags (custom_branding, api_access, priority_support, white_label, ai_enabled) and numeric limits (max_users, max_clients, max_sites, feature_usage_limit) read VERBATIM from the subscription_plans catalog row for the tenant's current plan_code, plus plan is_active and the tenant's feature-gate tier (tenants.subscription_tier). The read-side complement to get_my_subscription that makes 'displayed == entitled' verifiable from the tenant/MCP surface. Tenant-scoped — each tenant sees only its own entitlements.

  • set_tenant_plan_price_override

    Test-gated, tenant-scoped subscription-plan price override for autonomous, fully-reversible price-change testing inside a dogfood tenant. Sets (or clears with price_cents:null) a GST-inclusive override keyed on (this tenant, plan_code, billing_interval) that changes ONLY this tenant's Till-1 Checkout charge — the shared subscription_plans catalog and all other tenants are untouched.

  • set_tenant_feature_flag_override

    Test-gated, tenant-scoped entitlement feature-flag override for autonomous, fully-reversible entitlement-gating testing inside a dogfood tenant. Sets (value:true/false) or clears (value:null → fall back to the plan default) a per-tenant override of ONE entitlement flag (custom_branding, api_access, priority_support, white_label, ai_enabled) that overlays the shared subscription_plans catalog ONLY for THIS tenant. get_my_entitlements reflects the overlay, so 'displayed == entitled' is verifiable from the MCP surface without platform-admin browser auth and without touching the shared catalog or any other tenant.

  • create_billing_portal_session

    Creates a Stripe Billing Portal session for the current tenant. Returns a URL to redirect the operator to manage their subscription (update payment method, switch plan, cancel). Billing-exempt — works for any subscription status.

  • register_comms_account

    Register or update a comms mailbox account for the tenant. Upsert on (tenant_id, address) — first call creates, second returns existing with created:false. NO password field — mailbox credentials are managed via the settings UI only, never through MCP.

  • ingest_comms_messages

    Batch-ingest email messages into the comms layer (≤50 per call). Idempotent on content_hash — duplicate messages are counted but not re-inserted. Body text >64KB is truncated (body_truncated:true), never rejected. Returns {inserted, duplicates} counts.

  • list_comms_threads

    List comms threads for the tenant, optionally filtered by status or account_id. Returns exactly: id, account_id, thread_key, subject, status, last_message_at, created_at.

  • get_comms_thread

    Get a single comms thread with its messages and entity links.

  • link_comms_entity

    Link a comms thread to a business entity (client, contractor, site, invoice, proposal, work_order).

  • file_comms_work_item

    File a comms work item (triage or task) for human decision. The plain block is schema-validated (same shape as the Decision Queue: headline, what_happened, who_it_affects, where, if_we_do_nothing, options where every option MUST carry `recommended` (boolean) and exactly one recommended:true). Rejects with code plain_invalid on malformed blocks, listing EVERY violation at once with its limit and the length submitted.

  • list_comms_work_items

    List comms work items for the tenant, filterable by status, urgency, kind, and Decisions group.

  • decide_comms_work_item

    Approve, decline or REDIRECT a comms work item. The ONLY route to 'approved' status. Stamps decided_by from the authenticated identity and the given decision_channel.

  • record_comms_execution

    Record that an approved comms work item has been executed. Refuses with comms_item_not_approved unless the item is in 'approved' status.

  • create_comms_task

    Create a comms task (kind='task') — a personal or business to-do that needs no email thread.

  • upsert_comms_agent_note

    Create or update an agent note (epistemic memory) scoped to a sender, thread, entity, or globally.

  • list_comms_agent_notes

    List agent notes (epistemic memory), optionally filtered by scope and key. By default only returns non-superseded notes.

  • get_comms_brief

    Get the comms brief for a tenant — digest data including counts of items by urgency, NEEDS-YOU-NOW items, awaiting-decision items, recently executed items, and ingest health.

  • send_comms_digest

    Render and send the comms digest email for a tenant. Recipient MUST match the tenant setting comms.digest_recipients — any other recipient is refused with digest_recipient_not_allowlisted. Empty brief returns {skipped: true} without sending. Sends via existing platform mail machinery.

  • fetch_comms_history

    Fetch older email messages on demand from a connected mailbox. Unlike the background poller (which syncs only the last N days), this tool retrieves messages from ANY date range. Use it when investigating a thread and the relevant history predates the initial sync.

  • get_comms_extraction_queue

    Get messages queued for fact extraction — returns cleaned body text and attachment text ready for an extraction agent.

  • record_comms_facts

    Record facts extracted from an email message. Each fact must include a source_quote that is validated against the actual source text (body or attachment). If ANY quote fails validation, the entire call is rejected atomically — zero facts written.

  • resolve_comms_entity

    Resolve a comms entity — find matching clients, contractors, sites, invoices and bills from sender email, domain, or text references. Returns ranked candidates with score, matched_on, and match_reason. Returns an empty list rather than a low-confidence guess.

  • get_comms_triage_queue

    Get threads ready for triage — those with new messages or new extractions since last triage. Returns thread metadata, from addresses, work item status, and the FULL non-superseded fact set per thread (kind, label, value, source quotes, resolved entities). Every thread also carries `messages` — one envelope-only entry per message (id, from_address, folder, received_at, message_id, in_reply_to, is_outbound, fact_count), oldest first — so the pass can ORDER the conversation and tell whether an inbound arrived before or after our last reply, without inferring the date from a ledger row. Facts are the triage pass's entire view of message content: it NEVER reads message bodies, and must not call get_comms_thread (which returns bodies). Cross-check facts against the ledger via read-only tools (list_client_invoices, list_bills, list_payables, list_bank_transactions, get_client_money_story). When finished with a thread, call mark_comms_thread_triaged — always, even when nothing was filed.

  • backfill_comms_attachments

    Backfill attachment metadata + bytes for existing messages on one comms account. Scans messages that have no comms_attachments rows, addresses each by its exact Message-ID header via IMAP SEARCH, fetches bodyStructure + bytes, extracts PDF text, and stores attachment rows + source_documents.

  • backfill_comms_message_uids

    Give unaddressable mail its address back, so filing rules can act on it.

  • mark_comms_thread_triaged

    Mark a comms thread as triaged. Sets last_triaged_at and triage_state='triaged' so the thread leaves the triage queue until a new message or new extraction re-queues it.

  • get_comms_work_item

    Read ONE comms work item with its evidence resolved: cited comms_facts rows (kind, label, typed value, verbatim source_quote, source badge), ledger summaries (invoice number/status/total, bank transaction date/amount, client/contractor/site names), child items spawned from its adjacent opportunities, and its parent (provenance chain).

  • snooze_comms_work_item

    Defer a comms work item: hide it from the Needs-you group until `until` lapses, then it returns unchanged (status untouched — D35: snooze is deferral, not decision). Max 30 days (snooze_too_far beyond that — a longer deferral is a decline or a task).

  • accept_comms_adjacent

    Accept one of a work item's adjacent_opportunities: creates a LINKED CHILD task (kind='task', parent_work_item_id = the parent, dedupe_key adjacent:<parent>:<index>) that carries its own lifecycle — the provenance chain finding → decision → child → execution stays queryable forever (D34). The parent item is not modified. Repeat acceptance answers duplicate_dedupe_key.

  • draft_comms_reply

    Draft a reply INSIDE an existing email thread and put it in the mailbox's real Drafts folder. The reply anchors to a stored message: In-Reply-To and References are derived from that message's own header chain, and the subject gains exactly one 'Re: '. This is the only way to answer an email — a fresh out-of-context message from 'the system' breaks the recipient's conversation view, so cold outbound composition does not exist on this tool.

  • send_comms_reply

    Send a reply that was already drafted by draft_comms_reply, through the mailbox's own SMTP identity and into the existing thread. Files the copy in Sent, so the operator's mail client and the Decisions queue stay two views of one state.

  • list_mailbox_folders

    List the folders that actually exist in a tenant's mailbox. Read-only: this opens a connection, asks, and closes it.

  • create_mailbox_folder

    Create a folder in the tenant's mailbox. ADDITIVE ONLY — a folder that already exists is returned as-is (created:false), never replaced, and nothing is ever removed or renamed (D46).

  • move_comms_messages

    File mail into a folder — on the SERVER, in the operator's real mailbox, and on our row so the two never disagree. A move and nothing else: no copy is left behind, nothing is removed, no read-state or flag changes. Those verbs do not exist in this system (D46).

  • upsert_comms_filing_rule

    Create or update a filing rule — standing permission to move a class of mail into a folder on every future fire, without asking again.

  • list_comms_outbound

    List the replies this system has drafted and sent, with what became of each. Read-only.

  • get_counterparty_comms_log

    One read that answers "what have we said to this counterparty, and when?" — across every send path, without reading anybody's inbox. Read-only.

  • get_comms_mailbox_census

    Who is actually filling these mailboxes. A sender histogram over the stored messages: one row per account × folder × sender, with message and thread counts, the window they arrived in, up to three sample subjects, and whether an active filing rule already covers that sender. Read-only.

  • list_comms_filing_rules

    The standing permissions: every filing rule this tenant has, what it matches, where it files, whether it is active, and the approval it was born from. Read-only.

  • propose_mailbox_taxonomy

    Look at ONE connected mailbox and propose how it should be organised: which addresses land in it (direct, aliases, forwards — named verbatim), and a short folder set drawn from the tenant's shared spine (comms.taxonomy.spine — Clients, Suppliers, Contractors, Money, Automation, Newsletters by default), trimmed to the folders this mailbox's own mail would fill. Files ONE decision card for the mailbox (plain decision block: 'folder 1, folder 2 …' then Use these folders / Not yet) and stores the proposal as the mailbox's next taxonomy version. Senders it cannot place get their own small question cards (comms.taxonomy.side_question_min_msgs, comms.taxonomy.max_side_questions), never bundled into the proposal.

  • apply_mailbox_taxonomy

    Carry out an APPROVED mailbox folder proposal (the card propose_mailbox_taxonomy filed): record the folder set as how that mailbox is organised, make any missing folders, write one filing rule per recognised sender (each citing the card and the taxonomy), and move the existing mail under those rules.

  • execute_approved_comms_items

    Run the execution wire: carry out the staged actions of items the operator has ALREADY approved, so an approval at 7am is a coded ledger line by the next fire — never a bypass, never a decision. This tool never approves, decides, or sends anything; it executes only items a human has ALREADY approved, and only when their staged_action names an allowlisted verb with an args object.

  • escalate_comms_work_item

    Hand an approved item you could NOT carry out back to the operator, with the reason. Use this the moment an instruction cannot be honoured — a guard refuses, the world moved, the instruction contradicts live state, or you are not licensed for the verb it needs. The item lands at the TOP of the operator's queue with your reason on the card.

  • sweep_comms_execution

    Release abandoned execution claims and report every decided-but-unfinished item past its TTL, oldest first. An item past its TTL is an ALARM, not a status — twenty-eight items once sat approved for five days and nothing knew that was abnormal.

  • handoff_comms_work_item

    Pass an approved item to the desk that IS licensed to do it, instead of handing it back to the operator. Use this the moment you find the work is real and still wanted but the verb it needs belongs to another lane — raising an invoice is the bookkeeper's, filing mail is the steward's.

  • propose_send_credit_note

    Stages a credit-note send for operator approval. First leg of the three-leg send chain (propose → confirm → approve). Resolves the recipient email from the linked client's billing_email if not supplied. The ATO validation gate runs in approve_send_credit_note, NOT here.

  • annotate_document

    Writes the operator's MARKINGS onto a piece of evidence — the way a bookkeeper writes '60% business' across the top of an invoice, ticks the line items that are business, and rubber-stamps it ENTERED. The markings are stored as typed, queryable rows AND rendered into a NEW annotated copy that is linked back to the original. THE ORIGINAL IS NEVER CHANGED: its bytes and its sha256 are exactly what was uploaded, before and after. Nothing here computes anything — the expense record remains the computational truth; this is the operator's own hand on the paper.

  • generate_period_pack

    Generate the PERIOD PACK for a closed (or closing) period — the accountant's filing cabinet as one ZIP, with the folder tree `FY2027/Q1 Jul-Sep/2026-07 July/…`.

  • get_period_pack

    Read a generated PERIOD PACK: its manifest (every file with its sha256, size, source library and as-of stamp), the composed figures, and a short-lived signed download URL for the ZIP.

  • approve_send_statement

    Approves a staged statement send and dispatches the email to the client immediately. The statement transitions from draft → sent, message_id + recipient_email + sent_at are persisted, statement.sent is emitted.

  • get_compliance_document_for_review

    Returns a compliance document's metadata, readable content (text and/or images delivered in-band), and a short-lived signed URL (15 min). The reviewing agent can read the document directly from this tool's response — no URL fetch required.

  • upload_compliance_document_from_agent

    Uploads a compliance document (insurance certificate, etc.) on behalf of a contractor via the agent chat. Accepts the file as base64, validates integrity, stores it in Supabase Storage, creates a pending_review compliance document row, and syncs contractor denormalised fields. Mirrors the portal upload route exactly.

  • attach_receipt_from_agent

    Attaches a receipt or tax invoice the operator has ALREADY GIVEN YOU (PDF, JPEG or PNG, up to 10 MB) to ONE expense record (expense_id) OR ONE contractor bill (bill_id), in one call: stores the document, links it as PRIMARY EVIDENCE, records your reading of it (supplier, ABN, invoice number, date, total, GST), returns every place the paper disagrees with the books in `mismatches`, and re-derives substantiation. It NEVER corrects a figure and NEVER creates an expense — a mismatch is for the operator to resolve.

Authentication

Biloh MCP uses Bearer-token authentication. Tokens are tenant-scoped Personal Access Tokens issued by your administrator. Tokens carry an operator persona and respect the same RLS rules as the in-app user interface — a token cannot see or write data outside its tenant.

Self-service PAT issuance is under active development. Until that lands, request a token through your tenant's administrator surface or contact hello@biloh.com.au.

Reference

Ready to plug Biloh into your assistant?

Every business I set up gets its MCP endpoint on day one. Start with a call.