# Biloh — full documentation corpus > Every published page at https://biloh.com.au/docs, concatenated for AI agents. > See https://llmstxt.org/ for the llms.txt convention. > > Biloh is the AI-native operating system for service businesses, with Model > Context Protocol integration at every tier. MCP endpoint: > https://app.biloh.com.au/api/mcp # Delivery models: dispatch, self-perform, or both URL: https://biloh.com.au/docs/concepts/delivery-models Category: Explanation | Audience: operator | Updated: 2026-07-07 > A field business either dispatches work to subcontractors, does the work itself, or both. Biloh serves all three through a single tenant setting — subcontract, self_perform, or hybrid — that changes emphasis via progressive disclosure. Full triage control is always present; only the primary action shifts. A field-service business delivers work in one of three ways: it **dispatches** every job to subcontractors and earns the margin on the spread, it **self-performs** with its own crew, or it does **both**. Biloh serves all three postures through one tenant setting — `scheduling.delivery_model` = `subcontract` | `self_perform` | `hybrid` — rather than shipping two different products. The setting drives progressive disclosure, so full control is never removed; only the emphasis changes. ## Why one setting instead of two products? The two postures share almost everything — clients, sites, recurring cadences, invoicing, the whole event spine. What differs is which action an operator reaches for first. A dispatch-first operator wants Assign and Dispatch to lead; an owner-operator wants "I'm doing this one" and Complete to lead. Splitting that into separate products would fork the codebase over a difference of emphasis. A single configuration value captures it, and a second tenant flips postures by changing that value alone — no code path of its own. ## How each posture behaves - **subcontract** (default) — primary actions are Assign and Dispatch. Operator lifecycle controls (hold, cancel, complete-on-behalf) live behind the [Job Command Sheet](/docs/how-to/dispatching-and-triage), present but not shouting. - **self_perform** — adds a one-tap **Do it myself** (assign-to-self plus dispatch in a single move) and leads the run sheet with **Complete**. - **hybrid** — both, for the operator who self-performs but subs out the overflow. ## How does self-delivery keep the books straight? Your own crew is modelled as a real **internal contractor** row, not a special case. That means payables, contractor earnings, and completion evidence all flow through the same machinery whether a job is done in-house or handed to a subcontractor. Two tenant settings tune the money side: whether internal-crew jobs bypass the subcontractor compliance gate (they do by default — it's your own labour), and whether completing an internal job books a payable (off by default, because own labour is not a sub cost). ## What never changes Across every posture the non-negotiables hold: jobs are [scheduled by the day](/docs/concepts/scheduling-jobs-by-the-day), completed work is never mixed with active work, and the full triage sheet is always available. The delivery model is emphasis, not amputation. ## Related - [Dispatching and triaging jobs](/docs/how-to/dispatching-and-triage) - [Scheduling jobs by the day, not the minute](/docs/concepts/scheduling-jobs-by-the-day) --- # Scheduling jobs by the day, not the minute URL: https://biloh.com.au/docs/concepts/scheduling-jobs-by-the-day Category: Explanation | Audience: operator | Updated: 2026-06-26 > Field crews work a day, not a calendar slot. Modelling a job as "happens on this date" instead of "starts at 9:15" removes false precision, kills a class of timezone and drag-and-drop complexity, and matches how routes actually run. Recurring work anchors to an explicit start date, and edits choose between this occurrence and the whole future series. A field-service schedule models each job as *happening on a date*, not *starting at a time*. That single choice — day granularity instead of minute granularity — is what keeps the schedule honest about how the work actually happens, and it removes a surprising amount of accidental complexity. ## Why a day, not a time slot? A crew rarely keeps a 9:15 appointment the way a dentist does. They take a list of sites and work a route — mowing, cleaning, or servicing several properties across a morning in whatever order is efficient on the day. Pinning each job to a clock time is **false precision**: it implies a commitment the work doesn't make, and it drags in machinery that earns nothing — slot-collision rules, per-job timezone math, a calendar grid fine enough to need zooming on a phone. Model the job as "on Tuesday" and the schedule says what's true: this is the day it's planned for. The crew owns the order within the day. Rescheduling becomes "drag it to Wednesday," not "find a free 45-minute slot." (And on a phone, dragging a card across a day grid is hard enough — see [drag is a desktop idiom](/docs/engineering-notes/drag-is-a-desktop-idiom) — without also asking the operator to land on a time.) ## How recurring work stays stable A recurring service — weekly, fortnightly, monthly — is described by a recurrence rule with an **explicit start anchor**: the date the cadence is measured from. The anchor is deliberate, not "whenever this was last regenerated." If the rule re-materialises, a fortnightly job stays on its original week instead of silently flipping to the other fortnight. The anchor is the difference between a cadence that holds for years and one that drifts every time the schedule is rebuilt. ## Editing a series: the calendar model Recurring jobs borrow the mental model people already have from consumer calendars. An edit asks for **scope**: - **This occurrence** — move or change a single visit. The rest of the series is untouched. (Use it when one Tuesday is a public holiday.) - **All future** — change the cadence, rate, or assignment from here on. The rule is re-materialised and future planned visits are respun on the new pattern; visits already done are left alone. Keeping those two operations distinct is what lets an operator fix one bad date without rewriting the contract, and change the contract without hand-editing fifty visits. Which of those edits *bills* the client, and when, is a separate axis — covered in [when completing a job bills the client](/docs/concepts/when-completing-a-job-bills-the-client). ## Related - [When completing a job bills the client](/docs/concepts/when-completing-a-job-bills-the-client) - [Drag is a desktop idiom; the phone wants a tap](/docs/engineering-notes/drag-is-a-desktop-idiom) --- # How Biloh handles money: the two Stripe tills URL: https://biloh.com.au/docs/concepts/two-stripe-tills Category: Explanation | Audience: operator | Updated: 2026-06-26 > Biloh touches Stripe through two separate accounts. The platform account bills each business for its subscription. Each business connects its own Stripe account to charge its own customers — money settles directly to the business, which keeps its own payouts, disputes, and fees. Biloh handles money through **two separate Stripe accounts** — think of them as two tills that never share a drawer. - **Till 1 — platform billing.** Biloh's own Stripe account bills each business for its Biloh subscription (the monthly or annual plan). Money flows from the business to Biloh. - **Till 2 — your payments.** Each business connects **its own** Stripe account to charge **its own** customers. Money flows from the customer to the business. Biloh never sits in the middle of that drawer. Keeping the two tills separate is what lets Biloh bill you for the software while you, independently, get paid by the people you serve. ## Which till pays for what? Till 1 is the subscription you pay Biloh: a plan with a fixed monthly or annual price, charged on Biloh's platform account. Till 2 is everything you invoice your own clients for — a cleaning program, a quoted job, a deposit — charged on the Stripe account that belongs to you. ## How do I charge my own customers — do I hand Biloh my keys? No. You connect your **existing** Stripe account through Stripe's hosted authorisation flow ("Connect with Stripe"). You log into Stripe, approve the connection, and Stripe hands Biloh a connected-account id (`acct_…`) — never your secret keys. Because it is your own account: - Card payments settle **directly to you**. - You own your **payouts, refunds, disputes, and Stripe fees**. - Disconnecting later **revokes** Biloh's access on Stripe's side, not just locally. This is Stripe **Connect Standard** — connecting an account you already own and have already passed identity checks on — rather than a managed account Biloh creates for you. The trade-off is deliberate: you keep full ownership, and Biloh never holds your banking details or your keys. ## Why two tills instead of one? A single shared account would entangle the platform's revenue with each business's revenue — a reconciliation and compliance mess, and a real risk that one business's charge could be misattributed to another. Two tills keep the books, the bank accounts, and the liability cleanly separated. That same insistence on a forgery-proof boundary shows up in how payment webhooks decide who a charge belongs to: see [trusting the signed account over webhook metadata](/docs/engineering-notes/trusting-signed-webhook-fields). ## Related - [What is Biloh?](/docs/getting-started/what-is-biloh) - [The Biloh MCP, at a glance](/docs/reference/mcp-overview) - [Show the price you'll charge: one source of truth](/docs/engineering-notes/displayed-equals-charged) --- # When completing a job bills the client URL: https://biloh.com.au/docs/concepts/when-completing-a-job-bills-the-client Category: Explanation | Audience: operator | Updated: 2026-06-26 > Finishing a job can mean four different billing outcomes, chosen per client: draft an invoice for each job as it completes, accrue and consolidate a period into one invoice, hold until the period's last scheduled job completes and bill then, or never auto-bill. The completion event carries the decision; the cadence setting decides what happens. Completing a job doesn't have one billing meaning — it has four, and which one applies is **set per client**. The same recurring service can bill one client per visit and another once a month, because the cadence lives on the client, not the service. Here are the four outcomes a completion can produce. ## The four cadences - **Per job, on completion.** Each completed job immediately drafts its own invoice. Best for one-off or ad-hoc work where the client expects to be billed as things are done. - **Period, consolidated.** Completed jobs accrue across a period (say a month) and are drafted as a single invoice for that period — one line per visit. Best for clients who want one tidy bill, not a stream of them. - **Last job of the period triggers it.** The invoice drafts automatically when the *final scheduled job of the period* is completed. The schedule itself is the trigger: when the work for the month is finished, the bill exists. This is the powerful one for monthly contracts — there is no separate "run month-end billing" step to forget, because finishing the work *is* the month-end step. - **Manual.** Completion never auto-bills. The operator raises invoices by hand. Best where billing is negotiated, milestone-based, or handled outside the normal flow. > The most under-appreciated option is the third: let the act of finishing the last visit be the thing that bills the month. The event you already emit becomes the trigger you'd otherwise have to schedule. ## How the decision is actually made Completion emits an event — the canonical signal that a job is done. A cadence-aware handler reads the client's setting and decides: draft now, accrue for later, hold until the period's last job, or do nothing. Nothing about the *completion* changes between clients; only the handler's response to the event differs. That design is why getting the write path right matters so much. If a job is completed through a path that doesn't emit the event — see [one write path, many callers](/docs/engineering-notes/one-write-path-many-callers) — none of these cadences fire, and the most visible symptom is a completed job that never bills. And if the handler is implemented but never subscribed, you get the same silence for a different reason: [a handler nothing subscribes to does nothing](/docs/engineering-notes/wiring-an-event-to-its-handler). ## Why per-client, not per-platform Billing preference is a relationship fact, not a software fact. A property manager with twenty sites wants one consolidated monthly invoice; a one-off storm-damage cleanup wants billing the day it's done. Forcing both into a single platform-wide cadence would mean working around the tool for half your clients. Making cadence a per-client setting keeps the software out of the way of the commercial relationship. The schedule that produces these jobs is described in [scheduling jobs by the day](/docs/concepts/scheduling-jobs-by-the-day). ## Related - [One write path, many callers](/docs/engineering-notes/one-write-path-many-callers) - [A handler nothing subscribes to does nothing](/docs/engineering-notes/wiring-an-event-to-its-handler) - [Scheduling jobs by the day, not the minute](/docs/concepts/scheduling-jobs-by-the-day) --- # Show the price you'll charge: one source of truth URL: https://biloh.com.au/docs/engineering-notes/displayed-equals-charged Category: Engineering notes | Audience: builder | Updated: 2026-06-26 > The price a customer sees and the price you charge must come from one source. The moment a price is duplicated into a second surface — a static constant, a marketing page — the two can drift apart silently. Read prices live, and never bake a number into a place that can fall out of sync. A customer should never be charged a different number than the one you showed them. The only reliable way to guarantee that is to make the **displayed** price and the **charged** price read from the **same source** — the live plan record the checkout itself uses. The bug this prevents is quiet and corrosive: a pricing page reads a hard-coded constant while checkout reads the live plan table. Edit one and forget the other, and the page now advertises a price you don't charge. Nobody sees an error; a customer just notices later, and trust takes the hit. ## The rule Read prices **live** from the record of truth at render time. Do not copy a price into a second place — a constant in the code, a duplicated marketing string — that has to be kept in sync by hand. ## The corollary: don't show a price where you can't keep it honest Sometimes the safe move is to **not** show the number at all. A trial signup form, for example, can confirm the plan a visitor chose by **name** ("starting your trial on the Pro plan") without quoting a price — because a static price baked into the signup form is exactly the kind of duplicate that drifts. The authoritative price lives on the pricing page and at checkout, both reading the same source. Showing the plan name confirms the choice; omitting the static price removes a thing that can go wrong. ## How to check it The strongest check is a direct comparison: assert that the price the page renders equals the price the billing system would charge for that plan. If the two *can* disagree, eventually they will — so make a test prove they can't. ## Related - [How Biloh handles money: the two Stripe tills](/docs/concepts/two-stripe-tills) - [A handler nothing subscribes to does nothing](/docs/engineering-notes/wiring-an-event-to-its-handler) --- # Drag is a desktop idiom; the phone wants a tap URL: https://biloh.com.au/docs/engineering-notes/drag-is-a-desktop-idiom Category: Engineering notes | Audience: builder | Updated: 2026-06-26 > A drag-and-drop board that feels great with a mouse is often dead on a phone: a press-and-move is read as a scroll, cards never lift, and a calendar that only handles drops has no way to even open an item by tapping. Build the field surface tap-first and keep drag for the desktop planning view. A dispatch board built around dragging cards between columns can be excellent on a desktop and completely dead on a phone. The same gestures that feel natural with a mouse don't exist under a thumb, and the failure is silent: nothing errors, the cards just don't move and items won't open. If your users are in the field — and for field-service software they are — **the touch surface is the real surface.** Build it tap-first. ## Why the desktop board dies under a thumb Three concrete failure modes, all found on a board that worked perfectly with a mouse: 1. **No touch sensor.** The drag library was configured with pointer and keyboard sensors only. On touch, a press-and-move is interpreted as a page scroll, so a card never begins to drag. The fix is an explicit touch sensor with a short activation delay (a deliberate long-press), so the library can tell "I'm starting to drag this card" apart from "I'm scrolling the page." 2. **Drop without tap.** The calendar handled the drag-to-reschedule gesture but had no tap handler at all. Every event was inert to a finger — you could not even open a job to look at it. Drag and tap are different handlers; having one says nothing about the other. 3. **The card was a dead end.** Even where a card opened, it linked nowhere — no way to jump to the client or site behind it. On desktop you hover and right-click your way around; on a phone, if it isn't a tappable link, it doesn't exist. > On touch, a press-and-move is a scroll. Until you tell the interface otherwise, your drag gesture is the browser's scroll gesture. ## The deeper lesson: drag is for planning, tap is for the field You *can* make drag work on touch with the right sensors. But even done correctly, dragging a small card across a cramped grid one-handed, on the move, is a bad field experience. The interaction model — not the CSS — is the problem. A responsive skin on a drag-first board is still a drag-first board. So split the surfaces by what each is for: - **Desktop planning views** (a month calendar, a lane board) keep drag — it's fast for an operator allocating a day's work at a desk. - **The field surface is tap-first**: a run-sheet or list where you tap a job to open it and tap an action to advance it. No gesture that competes with scrolling, every target at least a finger wide, the next action one tap away. If you only have time to make one surface genuinely good, make it the tap-first one — that's the one a contractor uses in a driveway. This is the mobile-interaction sibling of [scheduling jobs by the day](/docs/concepts/scheduling-jobs-by-the-day): both are about matching the tool to how the work is actually done, not to how it's convenient to build. ## Related - [Scheduling jobs by the day, not the minute](/docs/concepts/scheduling-jobs-by-the-day) - [Verifying the tenant that isn't your default](/docs/engineering-notes/verifying-the-tenant-that-isnt-your-default) --- # The gap between a green build and a live deploy URL: https://biloh.com.au/docs/engineering-notes/green-build-vs-live-deploy Category: Engineering notes | Audience: builder | Updated: 2026-06-25 > A passing local build proves your code compiles — nothing more. The expensive failures in building this documentation corpus all lived in the gap between "it compiles" and "it's live": a deploy-time security gate, a missing shell variable that silently broke two tools, a popular framework that did not fit the stack, and a serverless function that could not read its own files. The defense is to verify across every boundary you cross. A passing local build tells you your code is syntactically valid. It does **not** tell you the thing will deploy, run, or be reachable. The most expensive hours of building this very documentation system were all spent in the gap between *"it compiles"* and *"it's live"* — and every one of them had the same shape: an assumption that held in one environment and quietly broke in the next. ## A green build is a claim about syntax, not about shipping A local production build passed cleanly. The deploy was then rejected — by a security gate that refused a known-vulnerable transitive dependency (a CVE in a markdown library), something the compiler never checks. The fix was a one-line patch bump; the lesson is durable: **"build passed" and "deploy succeeded" are independent claims**, and only the live deploy is authoritative. If your workflow lands on a single branch and every push deploys, treat the green deployment — not the green build — as your definition of done. ## When a tool goes silent, suspect the shell before your code Adding one dependency failed with a cryptic argument-type error, and the test runner produced *no output at all*. It looked like a corrupted install, and an hour disappeared into deleting and reinstalling things. The actual cause was a single missing environment variable — the path to the system command interpreter. Without it, both the package manager and the test runner could not spawn the child processes they rely on, and both failed in ways that mimicked broken code. The principle: **when several unrelated tools fail to *spawn* subprocesses, the common cause is the shell environment, not any one tool.** Check the environment before you start deleting your dependencies. ## A "production" install quietly removes your build tools Installing one new package in a shell that defaulted to production mode pruned the project's dev dependencies — including the CSS toolchain the build needs — and the next build failed with *module not found* for things that had been fine an hour earlier. Installs are environment-sensitive: when you add a package in a context that might be production-mode, force dev dependencies in (or set the environment explicitly) so the tool you just removed doesn't take the build down with it. ## Pick the engine for the stack you have, not the one the field recommends The most-recommended documentation framework required a major version of the web framework, the UI library, *and* the CSS toolchain that the project didn't run. Adopting it meant a high-risk upgrade of three foundations to ship a docs page. The right call was the boring one: a small, native library that matched the existing stack exactly. **Verify peer-dependency compatibility before you adopt the popular tool** — "best in class" is relative to your constraints, and the migration you avoid is the bug you don't ship. ## A serverless function can't read a file you didn't bundle A request-time route that read content files off disk worked locally and returned a 500 in production: the platform's output file-tracing can't follow a path computed at runtime, so the files were never shipped into the function. Two fixes work — declare the files for explicit inclusion, or make the route **static** so it reads at build time and serves a cached result. Runtime filesystem access is a deployment concern, not just a code concern. ## The data you parse is rarely the type you assumed An author wrote a date the natural way — unquoted — and the YAML parser handed back a `Date` object where the schema expected a string, failing the build. The fix was not to scold the author; it was to **coerce at the schema boundary**. Normalize inputs where they enter your system and be generous about the formats real authors will actually use. A validator that rejects the obvious, correct thing is a validator that will be worked around. ## The thread that ties them together Every one of these was a *boundary* failure: code → shell, local → deploy, build-time → run-time, parser → schema. A green check on one side of a boundary is not a green check on the other. The cheap, durable defense is two-part. First, **verify across the boundary you're about to cross** — run the thing in the environment that will actually run it, and treat the live deployment as the only completion signal. Second, **make the crossing safe**: a pre-push hook that runs the full build means a broken build is rejected at the push and never reaches production; a schema that coerces author-friendly input means a real author's first draft doesn't bounce; and a one-command check (here, `npm run docs:check`) that validates the cheap things — schema, links — in seconds means the slow boundary, the full build and deploy, only runs on changes already likely to pass. None of this is exotic. It's the same discipline as a good test suite, applied to the seams between systems rather than the inside of one. ## Related - [Lessons from shipping agent-facing MCP tools](/docs/engineering-notes/lessons-shipping-agent-tools) - [Making a multi-connector MCP setup safe to act on](/docs/engineering-notes/multi-tenant-mcp-safety) - [What is Biloh?](/docs/getting-started/what-is-biloh) --- # Lessons from shipping agent-facing MCP tools URL: https://biloh.com.au/docs/engineering-notes/lessons-shipping-agent-tools Category: Engineering notes | Audience: builder | Updated: 2026-06-25 > Five lessons that would have saved time on a multi-tool MCP build: test the tool the way the agent calls it, not just its inner function; the value you want to return is often already in hand; make implicit behaviour explicit; the build gate catches what unit tests don't; and a busy branch means rebasing. A run of agent-facing tools surfaced the same handful of lessons more than once. None are exotic; each cost real time the first time. Here they are as patterns to copy. ## 1. Test the tool the way the agent calls it, not just its inner function A tool's underlying function returned exactly the right value. Its unit test passed. The agent still got nothing — because the thin MCP wrapper between them **cherry-picked the response and dropped the new field.** The fix was an *integration* test that calls the tool through its wrapper and the response envelope, the way an agent actually does. It failed immediately, for the right reason. > The unit test proves the logic. The integration test proves the *consumer* sees the logic. Ship both. ## 2. The value you want to return is often already in hand A request to "return the bumped lock version after an update" looked like it needed a second read. It didn't. The database trigger that increments the version runs **before** the row is written, so the row already returned by the update carried the new value — it was just buried in a nested field the agent didn't know to read. The fix was to surface it explicitly, with no extra round-trip. **Read the existing behaviour before adding a query.** A surprising amount of "we need to fetch X" is "X is already on the object." ## 3. Make implicit behaviour explicit, in-band Two small changes removed real friction: - An opaque field name (`divergenceFlags`) became a self-describing one (`visits_per_year_mismatches`) at the response layer — without touching the value the UI consumes. - A tool that silently applied financial defaults began returning an `applied_defaults` block, so the agent could *see* what it had been given rather than infer it. An agent acts on what the response shows it. Surfacing the implicit is often higher-leverage than new logic. ## 4. The build gate catches what unit tests don't — run it The version changelog lives in a **single-quoted** string. An apostrophe in a description (`tool's`) closed the string and broke the build — twice. The test runner tolerated it; the production build (type-check plus lint) did not. Run the real build before every push, and judge it by its exit code or full output — never by the tail of the log, because lint errors appear early and a `tail` hides them. ## 5. On a busy main branch, expect to rebase — and preserve the other work When other commits land between your work and your push, the changelog and version files collide. The recovery pattern: 1. Rebase onto their commit. 2. Take their version of the changelog files, then **re-apply** your entry on top and bump your version *past* the collision. 3. Keep their changelog entry. Don't clobber it. Done that way, both histories survive and the version stays monotonic. A merge that overwrites someone else's changelog entry is a silent data loss. ## Next steps - The safety model these tools ship inside: [Making a multi-connector MCP setup safe to act on](/docs/engineering-notes/multi-tenant-mcp-safety). - How agents find these tools at all: [Tool discoverability for agents](/docs/engineering-notes/tool-discoverability-for-agents). --- # When the money arrives but can't be recorded URL: https://biloh.com.au/docs/engineering-notes/money-received-but-unrecorded Category: Engineering notes | Audience: builder | Updated: 2026-06-26 > A payment can succeed at the provider yet fail to record in your system — the invoice it belongs to was already paid, voided, or missing. That gap must raise an operator alert, while still returning success to the provider so it does not retry a charge it already captured. A payment provider confirms a charge succeeded. Your system tries to record it against an invoice — and can't, because the invoice was already fully paid, voided, or not found. **What happens next must never be silence.** The trap is easy to fall into: log a warning, mark the event "skipped," return `200 OK`, and move on. The customer has been charged, the money is sitting in the account, the invoice still reads *unpaid*, and **no human has been told**. That is a silent revenue-reconciliation gap — the kind that surfaces weeks later as an angry "I already paid this." ## The two-part rule 1. **Alert a human.** Emit a distinct "payment received but not recorded" event that drives an operator notification, carrying the invoice reference, the amount received, and the failure reason. Someone has to reconcile it by hand. 2. **Still acknowledge the provider.** Return success so the provider stops retrying. A retry would re-attempt the same record against the same unresolved cause and fail identically — and the money is already captured. Resolution is operator-side, not provider-side. Acknowledging the provider is **not** the same as pretending it worked. The acknowledgement is for the provider's retry logic; the alert is for the human who must fix the books. ## Why this needs its own path, separate from "payment failed" It is tempting to fold this into the generic "payment failed" alert, but the two are opposites. "Payment failed" means the customer was **not** charged — reassure them and let them retry. "Received but unrecorded" means the customer **was** charged but the books disagree — investigate immediately, and do **not** assume the invoice is unpaid. Different message, different urgency, different action. Collapsing them into one alert tells the operator the wrong story at the worst moment. ## Related - [Trust the signed account, not the webhook's metadata](/docs/engineering-notes/trusting-signed-webhook-fields) - [A handler nothing subscribes to does nothing](/docs/engineering-notes/wiring-an-event-to-its-handler) --- # Making a multi-connector MCP setup safe to act on URL: https://biloh.com.au/docs/engineering-notes/multi-tenant-mcp-safety Category: Engineering notes | Audience: builder | Updated: 2026-06-25 > When an agent holds several tenant connectors at once, the risk isn't data leakage (that's sealed server-side) — it's acting on the wrong tenant by mistake. The fix is layered: make the tenant visible, make it assertable, and gate the irreversible actions. Visibility alone is not enough. An AI agent can hold **several Biloh connectors at once** — one per tenant, plus a platform-admin connector — and they look almost identical in the tool list. The danger is not that one connector reads another's data (that is sealed by a tenant-bound token, row-level security, and write gateways, independent of the agent). The danger is **intent**: calling the right tool on the *wrong* connector. Biloh solves this in three layers, cheapest first: **make the tenant visible, make it assertable, and gate the actions you can't undo.** Visibility alone — the most common instinct — is not enough. ## Layer 1 — make the tenant visible Two signals, on by default: - **Connector identity.** Each connection advertises itself as `biloh · ()`, where mode is `LIVE` (a real tenant), `TEST` (a tenant flagged as test data), or `PLATFORM` (the cross-tenant admin surface). Two real tenants are *both* `LIVE` — so the **name** distinguishes them and the **mode** separates a tenant from the platform. - **A per-response stamp.** Every tool result carries `meta.tenant { id, name, mode }`. Because it is on *every* call, the last result an agent received always tells it which tenant it just touched. A subtle implementation note: the MCP handler is built **once per process**, so the server's identifying info is otherwise static across connections. Per-connection identity is therefore applied by rewriting the protocol `initialize` *response* — fail-safe, so any anomaly returns the original handshake untouched rather than risking the connection. ## Layer 2 — make the tenant assertable (a misroute should *fail*) Visibility helps a careful reader. It does nothing for a mistake already in flight. So a write can carry an optional `expected_tenant`: the tenant you *intend* to act on, by name or id. If it doesn't match the connector you're on, the call returns `expected_tenant_mismatch` and the handler **never runs**. A misroute stops instead of silently going through. The argument rides the same central schema-augmentation as cross-tenant routing and is enforced once, at the single wrapper every tool call passes through — so it covers the whole tool surface without per-tool code. > The principle: **data isolation and intent safety are different problems.** One is enforced by the database; the other has to be enforced by affordances the caller opts into. ## Layer 3 — gate what you can't undo Two-phase confirm-gates protect the irreversible actions. The first call returns a **preview** plus a deterministic `confirm_token` and mutates nothing; only a second call carrying the matching token executes. The scoping rule matters more than the mechanism: **gate by operation, not by keyword.** Archiving a tenant is irreversible — gated. Creating a tenant is gated too (it completes the lifecycle pair). Minting an access token is *revocable* — so it is deliberately **left ungated**. "Sounds dangerous" is not the test; "can't be undone" is. A gate is only safe to add after an **audit** confirms nothing automated calls the tool — otherwise you break provisioning. In this build the audit showed zero code-level callers, so the gate had zero blast radius. ## What each layer is locked by Every layer ships with a spec test written *before* the code, so the guarantee can't quietly regress: - The mismatch guard has a test asserting a wrong `expected_tenant` returns the error code and the handler does not run. - Each confirm-gate has a test asserting the no-token call previews and mutates nothing, a wrong token errors, and the matching token executes. - The identity rewrite is fail-safe by construction and tested against a malformed handshake. ## Next steps - The connection basics: [Connecting Biloh over MCP](/docs/reference/mcp-overview). - How an agent finds the right tool in the first place: [Tool discoverability for agents](/docs/engineering-notes/tool-discoverability-for-agents). --- # One write path, many callers URL: https://biloh.com.au/docs/engineering-notes/one-write-path-many-callers Category: Engineering notes | Audience: builder | Updated: 2026-06-26 > When the same change can be made from several places — a back-office screen, a field app, an agent tool — each entry point is a chance to drift. Route them all through one core function that owns the write and emits the canonical event. A caller that writes the row directly will look correct and silently skip every downstream effect. A job marked complete from the office drafted an invoice. The *same* job marked complete from the field app did not — even though both set the status to `completed` on the same row. The data looked identical afterward. The difference was invisible until someone asked why the money never appeared. The cause: the office path called a shared completion core that emits a canonical `job.completed` event; the field path wrote the status straight to the table. The billing handler listens for the event, not the column. So one caller fired the whole downstream chain and the other quietly skipped it. ## The rule: one core, many thin callers When the same logical change can originate from more than one place, **exactly one function should own that change** — perform the write, enforce the invariants, and emit the event. Every surface that wants to make the change calls that core: - the back-office UI route is a thin wrapper over the core, - the agent / MCP tool is a thin wrapper over the core, - the field or portal route is a thin wrapper over the core. None of them touch the table directly. Convergence is the point: a single place to read to know what *really* happens on a mutation, and a single place that guarantees the event is emitted every time. ## Why "it writes the same row" is a trap A second entry point that reproduces the database write looks correct in every way a human or a unit test usually checks: the row ends up in the right state. What it skips is everything that hangs off the *event* — invoicing, notifications, activity feed, downstream automations. Those are exactly the effects that don't show up in a quick glance at the record, so the divergence survives review and ships. > Two callers that write the same row are not equivalent if only one of them emits the event. The row is the same; the consequences are not. This is a sibling of a related failure — [a handler nothing subscribes to does nothing](/docs/engineering-notes/wiring-an-event-to-its-handler). There, the event fires and no consumer is wired. Here, the consumer is wired correctly but one caller never fires the event. Both end in the same place — a side effect that should have happened and didn't — so both deserve the same defense: test the real path end to end, not the function in isolation. ## Lock it with a structural test Convergence erodes the moment someone adds a fourth caller in a hurry. The cheap guard is a structural test that greps the route tree and **fails the build if any handler mutates the protected entity outside the core**. Write it as a glob over the routes, not a hand-maintained list of four files — the list is what rots; the next inline write lands in a file nobody added to it. Pair that with one integration test per transport that drives the *real* caller and asserts the downstream effect — complete via the field path, assert the invoice drafts; complete via the office path, assert the same. If a transport regresses to a direct write, its integration test goes red for the right reason. ## The thread The same discipline that keeps an agent tool honest — [test the tool the way the agent calls it](/docs/engineering-notes/lessons-shipping-agent-tools), through its wrapper — applies to every writer of a shared entity. Don't verify that the row changed. Verify that the *consequences* of changing it happened, through each door a user can walk in by. ## Related - [A handler nothing subscribes to does nothing](/docs/engineering-notes/wiring-an-event-to-its-handler) - [Lessons from shipping agent-facing MCP tools](/docs/engineering-notes/lessons-shipping-agent-tools) - [When completing a job bills the client](/docs/concepts/when-completing-a-job-bills-the-client) --- # Tool discoverability for agents URL: https://biloh.com.au/docs/engineering-notes/tool-discoverability-for-agents Category: Engineering notes | Audience: builder | Updated: 2026-06-25 > A catalogue of ~230 tools is unusable by name-scanning. The path is: orient with a categorised map, search by intent, then measure the one honest signal — the rate at which a search returns nothing. A miss is a catalogue gap, and it should be turned into a request for the missing tool. A Biloh tenant exposes on the order of **230 MCP tools**. No agent should find the right one by reading names. The working pattern is three moves — **orient, search, measure** — and the measurement is the part people skip. ## How does an agent orient? The first call on a fresh session is `get_session_context`. Beyond confirming the tenant and persona, it returns a **`tool_map`**: every tool the persona can use, grouped by category (clients, contractors, invoices, jobs, proposals, …), each with a one-line summary. That is the floor-plan — enough to know *where* a capability lives before narrowing. The authoritative flat list stays available as `mcp_health.tool_names`. The map is for orientation; the flat list is the source of truth. ## How does an agent find a specific tool? `search_tools("onboard a client for a recurring service")` returns a ranked, persona-scoped shortlist — name, summary, category. The agent searches by **what it wants to do**, not by guessing a name. One non-obvious discipline kept the ranker honest: **it carries no tool-name literals.** Ranking is pure lexical overlap (weighted name > category > description, with a small domain synonym map). A structural test asserts the ranker source contains no tool names, so it can't be quietly "tuned" by hard-coding the answers to the test queries — it has to generalise. The spec proves this on a *held-out* intent set the ranker never sees. ## What metric proves discoverability actually improved? The honest signal is the **zero-result rate**. If `search_tools` returns nothing, the catalogue or the ranker failed to serve the agent's intent — that is the gap to close, and it is the one thing worth counting. So a miss does three things: it sets an explicit `zero_results` flag on the response, it records a structured `tool_search` metric for aggregation, and it points the agent at the **request-a-tool** flow. That last move closes the loop: > search miss → tool request → catalogue grows → fewer misses. A high zero-result rate is not a failure to hide; it is a map of exactly what agents wanted that didn't exist yet. ## The shape to copy 1. A categorised map for orientation (`get_session_context`), a flat catalogue for truth (`mcp_health`). 2. Intent search with a ranker that has no knowledge of the test answers. 3. A zero-result signal that is both measured and turned into an actionable request. ## Next steps - Connection basics: [Connecting Biloh over MCP](/docs/reference/mcp-overview). - Keeping a multi-tenant connection safe: [Making a multi-connector MCP setup safe to act on](/docs/engineering-notes/multi-tenant-mcp-safety). --- # Trust the signed account, not the webhook's metadata URL: https://biloh.com.au/docs/engineering-notes/trusting-signed-webhook-fields Category: Engineering notes | Audience: builder | Updated: 2026-06-26 > A payment provider signs the connected account that owns a charge. That field is authoritative; metadata travelling alongside the event is not. Bind the payment to the signed account, refuse any metadata that disagrees, and clamp the recorded amount to what the provider actually received. On a platform that processes payments for many businesses, a payment webhook must answer one question safely: **which business does this money belong to?** The answer has to come from the field the provider **cryptographically signs** — the connected account that owns the charge — and never from free-form metadata riding alongside the event. We learned this the direct way. A webhook resolved the owning business from a `metadata.tenant_id` field. Because metadata can be set by whoever creates the charge, a connected business could attribute a payment to a **different** business — and an inflated amount field could record more than was actually captured. ## What the fix looks like Three rules, enforced at the webhook boundary: 1. **Bind to the signed account.** Resolve the business from the provider-signed `account`, which maps to exactly one connected business. That mapping is the source of truth. 2. **Refuse contradictory metadata.** If metadata names a *different* business than the signed account, reject the event and log it — do not silently prefer one over the other. 3. **Clamp the amount.** Record at most what the provider says it received. A metadata-supplied figure that does not reconcile to the actual amount received is ignored. Only events with **no** connected account — the platform billing itself — fall back to trusting metadata, because the platform created that charge on its own account. ## The test that locks it The guardrail ships with a spec test written **before** the fix and confirmed failing first: feed a signed event for account A carrying metadata that names business B, then assert the handler resolves to A, refuses B, and records no more than the amount actually received. A security invariant without a failing-first test is a hope, not a guarantee. ## The general pattern Whenever a provider signs some fields and leaves others open, treat the signed set as authoritative and everything else as untrusted input. This is the payments-specific face of the same principle behind [multi-tenant MCP safety](/docs/engineering-notes/multi-tenant-mcp-safety): isolation has to be enforced on a value the caller cannot forge. ## Related - [How Biloh handles money: the two Stripe tills](/docs/concepts/two-stripe-tills) - [When the money arrives but can't be recorded](/docs/engineering-notes/money-received-but-unrecorded) --- # Verifying the tenant that isn't your default URL: https://biloh.com.au/docs/engineering-notes/verifying-the-tenant-that-isnt-your-default Category: Engineering notes | Audience: builder | Updated: 2026-06-26 > A route returned 401 for a real tenant while every test passed and the deploy was green. The suite couldn't see it because the harness injects the tenant id directly — so the route's own resolution code never runs in tests. When your harness supplies the input a bug is about, it is structurally blind to that bug. A route returned `401 — no tenant context` for a real tenant. The full test suite was green. The build was green. The deploy was green. Every automated signal said the code was correct, and it was broken for an actual user the moment they loaded the page. The reason the suite couldn't see it is worth keeping: **the test harness injects the tenant id directly into the request.** Tests run with the tenant already resolved, so the route's *own* tenant-resolution code — the exact lines with the bug — never executes under test. The harness that exists to verify the route was quietly skipping the one part of it that was wrong. ## When the harness supplies the input, it can't test the input This is a general trap, not a one-off. A test fixture that hands the system a clean, pre-resolved value buys speed and isolation — and in exchange it stops exercising whatever produces that value in production. If the bug lives in the producer, the fixture is blind to it by construction. > When your harness supplies the very input the bug is about, it cannot see the bug. You have tested everything except the broken thing. Tenant resolution is the classic case: in-process tests inject the tenant so they don't need a real auth round-trip, and as a result the middleware and route logic that derive the tenant from a session or token go completely unrun. Auth, request parsing, and header derivation all have the same shape — cheap to stub, dangerous to leave unverified. ## The default tenant hides bugs too There's a second blind spot stacked on the first. The seed or default tenant — the one every developer reaches for — often resolves through a slightly different path or carries data the others don't. A bug in shared resolution can pass on the default and fail on every real tenant. So "I clicked it and it worked" is only true if you clicked it **as the tenant a real customer would be**, not the house account. ## The catch: adversarial use on a real tenant What actually found the 401 was a person opening the deployed app, signed in as a non-default tenant, and using it — not a green checkmark. That's the durable practice: after the automated signals pass, drive the live deployment by hand, on a tenant that isn't your default, and try to break it. This is a *different boundary* than the one in [the gap between a green build and a live deploy](/docs/engineering-notes/green-build-vs-live-deploy): there the failure was build-versus-deploy; here it's harness-versus-reality. Both reward the same move — cross the boundary the green check didn't. For the multi-tenant safety rails this verification protects, see [making a multi-connector MCP setup safe to act on](/docs/engineering-notes/multi-tenant-mcp-safety). ## Related - [The gap between a green build and a live deploy](/docs/engineering-notes/green-build-vs-live-deploy) - [Making a multi-connector MCP setup safe to act on](/docs/engineering-notes/multi-tenant-mcp-safety) - [Drag is a desktop idiom; the phone wants a tap](/docs/engineering-notes/drag-is-a-desktop-idiom) --- # A handler nothing subscribes to does nothing URL: https://biloh.com.au/docs/engineering-notes/wiring-an-event-to-its-handler Category: Engineering notes | Audience: builder | Updated: 2026-06-26 > A passing unit test proves a handler works when called — not that anything calls it. If no subscription wires the handler to its event, the event fires and dies with no consumer. Assert the subscription, and choose sync versus async delivery on purpose. We had a handler that issued a branded invoice when a subscription payment came in. It was implemented. It was unit-tested. It had **never run in production** — because nothing subscribed it to its event. The event fired on every payment and died with no consumer; no invoice was ever issued. The lesson is uncomfortable and worth internalising: **a green unit test on a handler proves the handler works when you call it, not that anything calls it.** The wiring — the subscription that connects an event type to a handler — is a separate fact that needs its own verification. ## How to verify the wiring, not just the logic Write a test that drives the **real dispatch path**: emit the event the way production emits it, then assert the side effect actually happened — the row was written, the email was sent. If the subscription is missing, that test fails even though the handler's own unit test is green. That difference is the entire point: one test exercises the function, the other exercises the connection. ## Sync or async — decide on purpose Once an event has a consumer, *how* it runs matters: - **Synchronous** handlers run inline as the event is emitted. Choose this for a side effect that **must** happen with the triggering action and cannot tolerate a queue that might not drain — for example, issuing a financial document the moment money is received. - **Asynchronous** handlers run later off a queue. Choose this for work that can lag a little — a notification email, a mirror to an external accounting system. Defaulting everything to async is convenient until the queue stalls and a money-critical document silently never appears. Match the delivery mode to the cost of the side effect not happening. ## Related - [When the money arrives but can't be recorded](/docs/engineering-notes/money-received-but-unrecorded) - [Show the price you'll charge: one source of truth](/docs/engineering-notes/displayed-equals-charged) --- # Why your brand assets should outlive your designer URL: https://biloh.com.au/docs/explanation/brand-pack-asset-custody Category: Explanation | Audience: evaluator | Updated: 2026-07-05 > A Biloh Brand Pack stores every brand asset — logos, brand guide, socials, signage — alongside its editable source file in your own Biloh workspace. When your designer changes, the next one picks up the actual masters, not screenshots. You can export the entire kit at any time. Most small businesses don't lose their brand in a rebrand — they lose it in a handover. The designer who built the logo moves on, the working files live on their laptop, and all the business keeps is a compressed JPEG. The next designer starts from scratch, and the phone number on the vehicle signage stays wrong for two more years. ## The custody model A Biloh Brand Pack treats brand assets as **business records, not designer property**. Every asset in the kit has two parts, stored together in your Biloh workspace: **The finished file** — the production-ready export (PNG, PDF, SVG) that your invoices, website, email signature, and social profiles actually use. **The editable source** — the master the designer worked in: Illustrator, Figma, Photoshop, Affinity, whatever their tool of choice. Biloh is deliberately provider-agnostic; designers upload from any workflow. Because both live in your workspace, the kit is *designer-independent*. When your details change — a new phone number, a new address — any designer can download the master, make the edit, and upload the new finished file plus the updated source. The chain never breaks. ## What "the whole kit" means A logo is not a brand. The Biloh kit covers the thirteen assets a service business actually needs: the full logo suite (horizontal, square mark, and reversed-for-dark versions), the brand guide document that records your colours, fonts and usage rules, an email header and an email signature block, the social set (profile avatar, page banner, reusable post template), a letterhead and a print-resolution PDF header, a favicon, and a print-ready vehicle signage layout. Each slot in the kit carries its exact specification — format, dimensions, background, colour mode — so designers deliver the right thing the first time, and you can see at a glance what's complete. ## How the work gets done Biloh coordinates the engagement end to end: you buy the pack, Biloh commissions one of its vetted Australian design vendors, and the designer delivers into a purpose-built portal that lists every asset with its spec and accepts both the finished file and the source master. When they submit the kit for review, you're notified. If you later want a refresh, the next engagement starts from the real masters. And if you ever want out: one click exports the entire kit — every file, every source, plus a generated brand guide — as a zip. It's your brand. *The Brand Pack is available at [biloh.com.au/brand-pack](https://biloh.com.au/brand-pack), alongside Biloh's other professional services at [biloh.com.au/store](https://biloh.com.au/store).* --- # What is Biloh? URL: https://biloh.com.au/docs/getting-started/what-is-biloh Category: Explanation | Audience: operator | Updated: 2026-06-25 > Biloh is an AI-native operating system for service businesses. It runs quoting, scheduling, contractor dispatch, and invoicing through chat and any MCP-compatible AI agent, with field-ready portals for clients and contractors — and Model Context Protocol integration bundled at every tier. Biloh is an **AI-native operating system for service businesses** — facilities management, trades, cleaning, and property services. It runs the work a service business actually does: quoting jobs, scheduling recurring visits, dispatching contractors, and invoicing clients. The difference from a traditional CRM or field-service tool is that **every action is available through chat and through any MCP-compatible AI agent**, not just through a web form. ## What can Biloh do? | Job to be done | In Biloh | | --- | --- | | Quote work | Build a proposal, send it, and capture a legally-binding acceptance | | Schedule recurring work | Set a cadence once; jobs spawn on their true dates | | Dispatch contractors | Assign, send a work order, and gate it on compliance | | Bill clients | Generate a tax invoice, take payment, and reconcile | | Pay contractors | Track payables and release them against client payment | Every one of those is a tool an AI agent can call, so you can run your business by typing "send the Riverside Bakery its quarterly invoice" instead of clicking through five screens. ## Who is it for? Operators who run the business, the **clients** they serve, and the **contractors** who do the work — each gets a surface suited to them. Operators work chat-first with a web app alongside; clients and contractors use field-ready portals from the same phone they already carry. ## Why "AI-native"? Most software bolts a chatbot onto a clicking-based app. Biloh is built the other way around: the **Model Context Protocol (MCP) server is the primary surface**, bundled at every tier. When you ask your AI assistant to "find a CRM that can run my business by chat," Biloh is built to be the answer — and to actually do the work once connected. ## Next steps - Connect Biloh to your AI assistant — see the [MCP integration overview](/docs/reference/mcp-overview). - Send your first quote — see [how to send a proposal](/docs/how-to/send-a-proposal). --- # Dispatching and triaging jobs URL: https://biloh.com.au/docs/how-to/dispatching-and-triage Category: How-to guides | Audience: operator | Updated: 2026-07-07 > Every job has one status-aware action surface — the Job Command Sheet — reachable from any board view. Assign or do it yourself, dispatch, reschedule with a reason, hold, cancel, or complete on behalf. A needs-attention strip surfaces overdue, missed, and soon-unassigned work so nothing goes invisible. To dispatch and triage work in Biloh you use one status-aware surface — the **Job Command Sheet** — reachable from every board view. You can do each step by chat (ask your AI assistant) or in the app; the actions are identical. ## How do I get a job to a contractor? Open the job's card and the Job Command Sheet appears with the actions that make sense for its status. For an unassigned job you can **Assign** it to a compliant contractor, or — if you do the work yourself — **Do it myself**, which assigns to your own crew and dispatches in one tap (see [delivery models](/docs/concepts/delivery-models)). Once assigned, **Dispatch** hands it to the contractor. Assignment runs a compliance gate: a contractor who lacks the service capability, a signed agreement, or current insurance is blocked with the exact reason, not silently dropped. ## What can I do from the sheet? The sheet is the whole triage menu, so control never lives on some other screen: - **Reschedule** — move this one occurrence to a new day, with a reason. Editing a recurring series instead asks whether you mean this occurrence or all future ones ([scheduling by the day](/docs/concepts/scheduling-jobs-by-the-day)). - **Reassign / Unassign** — hand the job to a different contractor, or pull it back to the unassigned pool. - **Hold / Resume**, **Cancel** (reason required), and **Complete on behalf** — record a completion for a job done in the field, honouring the site's photo and notes requirements. - **Un-complete** — undo a completion (until the job has been invoiced), the way Housecall Pro's "unfinish" works. ## What needs my attention this morning? The board leads with a **needs-attention strip**: Overdue, Missed, Unassigned within the next few days, Declined, and Awaiting re-acceptance. Each chip filters the list to exactly those jobs. The design rule is that **every fetched job appears in at least one bucket** — a job whose date has passed surfaces in Overdue rather than falling into a gap. Tap a chip, work the list, and the count drops as you clear it. ## Bulk moves after a bad-weather day When a whole day needs to shift — a public holiday, a rain-out — select the affected jobs and **bulk-reschedule** them to one new date with a shared reason. Bulk gestures are a Pro-plan capability; single-job reschedule is always available. The move records a per-job audit trail and moves only the jobs whose lifecycle allows it, returning any it couldn't move. ## Related - [Delivery models: dispatch, self-perform, or both](/docs/concepts/delivery-models) - [Scheduling jobs by the day, not the minute](/docs/concepts/scheduling-jobs-by-the-day) - [Route optimization: the runs bolt-on](/docs/reference/route-optimization-bolt-on) --- # How to send a proposal URL: https://biloh.com.au/docs/how-to/send-a-proposal Category: How-to guides | Audience: operator | Updated: 2026-06-25 > Create a proposal for a client and site, add priced service lines, preview the honest investment summary, then send it. The client accepts by replying or via a portal link, which forms a binding acceptance and stands up the contract. To send a proposal in Biloh you build it, preview it, and send it for acceptance. You can do every step by chat (ask your AI assistant) or in the web app — the steps are the same. ## 1. Create the proposal Create a proposal against a **client** and one of their **sites**. If the client or site does not exist yet, create them first (or use the one-step onboarding composite). A new proposal starts in `draft`. ## 2. Add priced service lines Add a line for each service you are quoting, with its price and — for recurring work — its cadence (weekly, monthly, a multi-visit program, or specific dates). Pricing is captured in integer cents and shown to the client as a clear investment. ## 3. Preview the investment summary Before sending, preview the **investment summary**. This is the same honest, frequency-aware figure the client will see on the signed agreement and the accept page — visits per year are placed on their true months, not evenly smeared. Previewing here means there are no surprises at acceptance. ## 4. Send it Sending is deliberately two-step: stage the send for approval, then approve it. On approval Biloh emails the client a branded proposal with a portal link. ## 5. The client accepts The client accepts by replying to the email or via the portal. Acceptance is captured as immutable legal evidence under the Electronic Transactions Act, and the accepted proposal stands up the contract and its schedule automatically. ## Related - [What is Biloh?](/docs/getting-started/what-is-biloh) - [Connecting Biloh over MCP](/docs/reference/mcp-overview) — to do all of this by chat. --- # Connecting Biloh over MCP URL: https://biloh.com.au/docs/reference/mcp-overview Category: Reference | Audience: builder | Updated: 2026-06-25 > Biloh exposes a Model Context Protocol server at app.biloh.com.au/api/mcp. Authenticate with a tenant-scoped Personal Access Token, and any MCP-compatible assistant can drive the tenant's operations end-to-end. MCP is included at every tier. Biloh exposes a **Model Context Protocol (MCP) server** so any MCP-compatible AI assistant — Claude, ChatGPT, Grok, Perplexity — can run a tenant's operations end-to-end. This page is the reference for connecting. ## Where is the endpoint? The MCP server lives at: ``` https://app.biloh.com.au/api/mcp ``` It is a streamable HTTP MCP server implementing the [Model Context Protocol](https://modelcontextprotocol.io). MCP integration is included at **every** Biloh tier. ## How does authentication work? Requests authenticate with a **tenant-scoped Personal Access Token (PAT)**. The token binds every call to a single tenant, so a connection always acts on exactly one business's data — isolation is enforced server-side by row-level security and write gateways, independent of the agent. ## What can an agent do once connected? Orient first, then act: - **`get_session_context`** — the read-me-first call: returns the active tenant, your persona, conventions, and live state. - **`search_tools(query)`** — find the right tool by intent instead of scanning the full catalogue. - Composite tools like **`onboard_client_for_service`** set up a client, site, and pricing in one call. Sends are always two-step (`propose_send_*` then `approve_send_*`) so nothing leaves the system without an explicit approval. ## Next steps - New to Biloh? Start with [what Biloh is](/docs/getting-started/what-is-biloh). - Want to send your first quote by chat? See [how to send a proposal](/docs/how-to/send-a-proposal). --- # Route optimization: the runs bolt-on URL: https://biloh.com.au/docs/reference/route-optimization-bolt-on Category: Reference | Audience: operator | Updated: 2026-07-07 > A run is a first-class projection over a day's jobs: an ordered list of stops. Building a run and ordering it by hand is base functionality. The optimizer that sequences stops for you is a paid, per-account bolt-on — off by default — and it is the only place an external solver is ever called. A **run** is a first-class projection over a day's jobs: an ordered list of stops for one crew on one date. Runs let you plan the shape of a day — which sites, in which order — without ever pinning a job to a clock time. Building and hand-ordering a run is base functionality; the automatic optimizer is a paid bolt-on. ## What is a run made of? A run has a date, an optional crew, and a set of **run stops**. Each stop points at a job and carries a `stop_order` — its position in the sequence — plus an optional lock so a fixed stop stays put. A job appears in at most one active run per day. Runs are deletable without touching the schedule: they are a view over jobs, not a rewrite of them. Stops with no location are listed separately and never silently dropped. ## What's free and what's the bolt-on? Creating a run and **ordering it by hand** — drag a stop up or down, lock the one that has to be first — is available on every plan. The **optimizer** that sequences stops for you is a separate, per-account **paid bolt-on**, off by default. It is enabled per tenant by configuration alone, and it is the *only* place an external routing solver is ever invoked. Until it is enabled, the run sheet is fully usable with manual ordering; there is no optimize button. ## How does navigation hand off to maps? Once a run is ordered, each stop can hand its destination to a maps app for turn-by-turn driving. Whole-run export respects a hard external limit: a consumer maps directions URL accepts at most **nine waypoints** in the app (three in a mobile browser), so a long run is split into labelled segments rather than emitting a link that would silently truncate stops. Per-stop navigation is the primary path; whole-run export is the convenience. ## Tunables The bolt-on's behaviour is configured through platform settings (editable in the app and by an AI assistant at the same depth): which solver adapter backs the tenant, whether to balance a fleet by time or by stop count, whether to favour clean geographic clusters, a daily optimization cap as a cost guard, and a default per-stop service duration. These are inert until the bolt-on is switched on. ## Related - [Dispatching and triaging jobs](/docs/how-to/dispatching-and-triage) - [Scheduling jobs by the day, not the minute](/docs/concepts/scheduling-jobs-by-the-day)