# LatticeKit > A platform for building operational business software. When a business needs > scheduling, customers, staff, resources, products, payments, a ledger, workflows, > communications or reporting, LatticeKit composes those into an application for that > business — and an AI agent can build one from a plain-language description, put a > live demo in front of the owner, create the account, and keep configuring it through > the agent API, with consequential changes parked for the owner to approve. These documents are written for people and programs working *against* a LatticeKit business, rather than for its authors: the domain model, the contracts that hold everywhere, and how to connect. Start with "What LatticeKit is". To build a business for someone, read "Agent API" — the section "Creating a business from an agent". The same set is served to connected agents as MCP resources at `POST /agent/v1/mcp` and at the pre-tenant `POST /agent/v1/signup` (`resources/list`, `resources/read`), addressed as `latticekit://docs/...`. --- --- title: "Agent API" summary: "How an AI client connects to a LatticeKit business over MCP: the endpoint, how a grant is consented to and revoked, what the three scopes permit, why consequential actions park for a human, and how to find the right capability." --- # Agent API LatticeKit speaks the Model Context Protocol. An AI client outside the platform can connect to a single business and work in it — reading the business, proposing changes, and verifying that the changes worked. ## The endpoint One route, streamable HTTP, stateless: one JSON-RPC message in, one JSON response out. It supports `initialize`, `ping`, notifications, `tools/list`, `tools/call`, and `resources/list` / `resources/read` for documentation. JSON-RPC batching is not supported. Clients discover where to authenticate through standard protected-resource metadata, then register dynamically and walk authorise → consent → token. ## Grants Everything hangs off a **grant**, not off the credential. An operator sees a consent screen naming the client and chooses what it may do. Revoking the grant stops the client immediately, without rotating or reissuing anything. Scopes live on the grant: | Scope | Permits | |---|---| | `agent:read` | Reading the business — schedules, availability, catalog, customers, orders, communications history. | | `agent:write` | Tier-one actions only: check in a booking, record attendance, submit a form response. | | `agent:propose` | The right to *ask* for a consequential action. Calling one does not perform it. | A grant can also be narrowed to one location, or marked sandbox-only — which forces neutralised side effects on the server, so a client cannot opt out by omitting a header. ## Why writes are tiered `agent:write` is not "may do anything". It unlocks a small, deliberately boring set: the repetitive actions a door scanner, a register or a kiosk performs, where nobody was supervising any individual one anyway. Everything above that tier parks. Calling it creates a proposal describing what would happen and what it would cost to be wrong, and a person decides. This boundary lives in the platform rather than in a prompt, so it holds regardless of what a model was persuaded to believe. Every write requires an idempotency key. See [errors](errors.md) — the retry here is a machine's, and machines retry without knowing whether the first attempt landed. ## Finding the right tool `tools/list` advertises a small fixed core rather than the full surface, because agents degrade under schema sprawl. Discovery beats enumeration: - `lattice_search_capabilities` — keyword search across everything this grant can reach. - `lattice_describe_capability` — the full typed schema for one, and what it costs to be wrong. - `lattice_invoke` — call any capability the grant permits, by id. Anything visible stays callable through `lattice_invoke` whether or not it is advertised. The short list is a context optimisation; the boundary is the grant. ## What an agent cannot do - Reach an action a human operator could not, or reach it under weaker rules. The same checks apply to every caller. - Execute a consequential action without a human. It can only ask. - Exceed its daily ceilings on calls and money — a loop that goes wrong is bounded by policy, not by whoever notices first. - See or write outside its granted scope. See [tenancy](tenancy.md). - Act unaudited. Every call is recorded against the grant. ## Creating a business from an agent An agent can also take a person from a description of their business to a live demo of it to an account that keeps that demo — and come out the other side holding a grant to it. This runs on a second, pre-tenant MCP endpoint, `POST /agent/v1/signup`, with the same protocol shape as the one above (`initialize`, `ping`, `tools/list`, `tools/call`, and the documentation resources) and a fixed set of tools. **Credentials are arguments here.** There is no authorization server for this endpoint and nothing to discover: a conversation's `secret` and a signup's `agentToken` are each returned once, when the thing is created, and passed back as arguments on every later call about it (the signup token is also accepted as `Authorization: Bearer`, for clients that can set one). A credential given as an argument that fails is a tool result with `isError: true`, never an HTTP 401. Every tool declares `securitySchemes: [{type: "noauth"}]`. Two things stay the person's and cannot be done by the agent: confirming the email (where they also accept the terms and allow the agent's access), and — only while the platform collects a card at signup — putting a card on file on the payment processor's page. The agent relays instructions and links; every result carries a `nextStep` written to be relayed. ### What is open 1. **`lattice_signup_mode`** — no credential. Returns `{ mode, waitlistOpen, cardRequired, conciergeMode, demoMode }`. While the platform is invite-only (`mode: "invite_only"`) every account tool below answers `403` with problem type `signup-closed`; the concierge tools answer to `conciergeMode` instead (`off` refuses `lattice_conversation_open` with `concierge-closed`). `cardRequired: false` means a confirmed email is the last step before the business exists. `demoMode: "on"` means live demo sites are being built. 2. **`lattice_starters`** — no credential, zero cost. The ten example businesses (a racquet club, gym, salon, studio, restaurant, barbershop, spa, clinic, hotel, music school), each with a summary and the default setup a preview of it has. Show them to someone who wants to look at an example, or begin from one. ### Describing, previewing, and a live demo 3. **`lattice_conversation_open { starter?, clientName }`** — no credential. Opens a conversation with the concierge, from a starter or from scratch. Returns `{ conversationId, secret, state, nextStep }`; the `secret` is shown once. Nothing is created. 4. **`lattice_conversation_say { conversationId, secret, text }`** — relay what the owner says about their business, in their words (at most 2000 characters a message), and get the concierge's reply, which may be a question to relay back. The transcript is what the preview is drafted from. `ready: true` means the concierge has enough. Under `conciergeMode: "free"` the reply comes from these documents rather than a model. Conversations are capped: twelve model turns, two previews, seven days. 5. **`lattice_conversation_preview { conversationId, secret, email, businessName? }`** — draft the business behind the owner's email: offers, hours, resources, staff, pricing, as LatticeKit would set it up. Returns `{ source, degraded, blueprint, state, nextStep }` and keeps the blueprint on the conversation. 6. **`lattice_conversation_demo { conversationId, secret }`** — build a live, bookable demo site of the previewed business: a real tenant at its own address, with the drafted offers and hours, that the owner can open and book in right now. Takes about a minute. One demo per conversation; it winds down after a week unless the owner creates the business, which then keeps it as-is. Refusals: `demo-no-starter` (a described business is not tied to a starter yet — the message lists the keys; pick one with **`lattice_conversation_starter { conversationId, secret, starter }`** and call again), `demo-closed`, `demo-capacity`, `demo-starter-unavailable`, and `demo-build-failed` (retry). Building is rate-limited per address. 7. **`lattice_conversation_state { conversationId, secret }`** — the transcript, the blueprint, the demo (its address and whether it is live), turns remaining, and what to do next. ### The account 8. **`lattice_signup_start { email, businessName, slug, clientName, conversationId?, secret? }`** — no credential. With the conversation, the business is created from it: its demo becomes the business, nothing is rebuilt, and its blueprint seeds the setup assistant; the conversation must have a preview or a starter (`concierge-state` otherwise). Without one, the account starts empty. `slug` becomes the business's address and must be free; `clientName` is what the person will see when asked to allow your access. Sends the confirmation email and returns `{ signupId, agentToken, conversationId, status, cardRequired, expiresAt, nextStep }`. The `agentToken` (prefix `sgat_`) is shown once and lives as long as the signup (48 hours). Nothing has been created yet. Terms are **not** accepted here — the person accepts them, and confirms that `clientName` may access the business once it exists, on the confirmation page. A repeat start for the same email inside the re-send cooldown is refused; one whose email has already been confirmed elsewhere answers `409`. 9. **`lattice_signup_status { signupId, agentToken }`** — returns `{ status, emailVerified, termsAccepted, agentAccessAllowed, cardOnFile, cardRequired, conversationId, tenantId, grantClaimed, expiresAt, nextStep, paymentUrl? }`. When a card is required, once the email is confirmed and the terms accepted it carries `paymentUrl` — the link the person opens to put a card on file; when they return the business is created and they are signed in as its owner. When no card is required, confirming the email creates the business at once (the person lands signed in), and `status` becomes `completed` on the next read. 10. **`lattice_signup_payment_link { signupId, agentToken, fresh? }`** — the same link on its own, for clients that want it separately. Refused before the email is confirmed or the terms accepted, and once a card is on file. Pass `fresh: true` only if the person reports the page has expired. 11. **`lattice_signup_complete { signupId, agentToken, slug? }`** — create the business now. Only once the email is confirmed and the terms accepted, and only when no card is required or one is on file (`signup-payment-method-required` otherwise, with the payment link to relay). The confirmation page creates the business itself under the no-card policy, so a `409` here means it already exists: claim the grant. `slug` changes the address, for a `tenant-slug-taken` answer. 12. **`lattice_signup_claim_grant { signupId, agentToken }`** — only once `status` is `completed`, and only once. Mints a grant on the new business — scopes `agent:read` + `agent:propose`, 30 days, 500 calls a day — and returns `{ apiKey, grantId, mcpUrl, tenantId, scopes, expiresAt, dailyCallLimit, nextStep }`. The `apiKey` is shown once; use it as the bearer at `mcpUrl`, the tenant endpoint described at the top of this page. A second claim answers problem type `signup-grant-claimed`. What the person does, and where: on the confirmation page they accept the Terms of Service and Privacy Policy and tick "Let *clientName* access *businessName* once it is created" — both are required, and the agent cannot supply either; when a card is required, on the payment page they add one, which never passes through LatticeKit or the agent. They may open the payment link in a different browser from the one that confirmed the email. A conversation an agent opened gets no welcome mail of its own; the confirmation email is the one message the person receives before the business exists. The grant the agent receives is the owner's decision, made on that confirmation page. It appears in the console under Agents like any other connection and can be revoked there at any time; it cannot write or move money, only read and propose. Refusals: a signup tool with neither a bearer nor an `agentToken` argument is `401` (with a bare `Bearer` challenge); a token for a different `signupId` is `403`; an expired signup is `410`. Everything an agent can act on — validation, a wrong or missing conversation secret, the re-send cooldown, the wrong state, every concierge and demo refusal, an already-claimed grant — comes back as a tool result with `isError: true`, its text ending in the problem type and HTTP status it corresponds to. ## Documentation This document set is served over the same connection as MCP resources, addressed `latticekit://docs/…`, and published as `llms.txt` for clients that have not connected. It is the same set either way. ## Configuring a business The interesting thing to do here is not fifty ordered calls. It is one document — see [tenant as code](tenant-as-code.md). --- --- title: "Customers, consent and communications" summary: "How LatticeKit models the people a business knows, how they are grouped into households, tiers and staff rosters, and why every outbound message is checked against recorded consent and refused when it is absent." --- # Customers, consent and communications ## People A **person** is a human the business knows — a customer, a member, a member of staff, a guest on someone else's booking. One person record, whatever roles they hold: somebody who books a court, works two shifts a week and is on a family membership is one person, not three. People carry contact details, [consents](#consent), and attributes the business defines for itself. What a gym needs to record about a member is not what a salon needs, so the attribute set is the business's to define rather than a fixed schema. ## Groups A **group** is the universal "this person belongs to something" model. Households, membership tiers, teams, class rosters and staff are all groups, differing by kind rather than by having separate systems. This is why a membership tier can gate a booking, a household can share a balance, and a staff rota can drive who is allowed through a door — they are the same mechanism asked different questions. Membership in a group can be ranked, which is what makes "primary account holder" or "lead instructor" expressible without a special case. ## Consent Consent is recorded per person, per channel, and per purpose. Marketing email is a different consent from a booking confirmation by SMS. **Communications are checked against consent and refused when it is absent.** This is the important behaviour and it is worth stating plainly: the check fails **closed**. If consent cannot be established, the message is not sent. There is no path that sends first and reconciles afterwards, and no way for a caller to assert consent it does not have. The practical consequence for an integrator: a send can be refused for consent reasons, and that refusal is a normal, expected outcome rather than a fault. Handle it — see [errors](errors.md). Some communications are exempt because they are not marketing and the person initiated them: a sign-in link, a password reset, a receipt for something just bought. The exemption is narrow and defined by the platform, not by the caller. ## Channels Messages go out over email, SMS, push notification, webhook, or in-app, behind one interface. A message is composed from a template the business controls, so wording, branding and language are configuration rather than code. Sending is subject to per-business daily caps per channel. This protects the shared sender reputation from one runaway business; an over-cap send is recorded and refused rather than silently dropped, so it is visible. ## Entitlement Whether a person may do a thing — book this offer, enter this door, receive this discount — is asked as a question rather than stored as a flag, because the answer depends on current group membership, tier, and any conditions the business wrote. See [expressions](expressions.md). Asking rather than storing is what stops entitlement drifting out of date when somebody's membership lapses. --- --- title: "Errors and how to act on them" summary: "LatticeKit returns machine-readable problem documents rather than prose, and many failures carry a remediation naming what to do next — the contract a program or an agent should code against." --- # Errors and how to act on them A growing share of callers are programs. A failure that only a human can interpret is a failure that stops an automation, so LatticeKit returns errors in a fixed, machine- readable shape and, where it can, says what to do next. ## The shape Failures are returned as problem documents — a standard structure with a `type`, a short `title`, a `status`, and a `detail` explaining this particular occurrence. Content type is `application/problem+json`. ```json { "type": "https://foundation.dev/problems/booking-slot-unavailable", "title": "The slot is no longer available", "status": 409, "detail": "Another booking took the last place at 09:00 while this request was in flight." } ``` Error responses are never hand-built prose and never plain strings. If you are parsing an error message to work out what happened, parse `type` instead — it is stable, and the human-readable fields are not. ## Remediations Some failures carry a **remediation**: a structured hint naming the concrete next step. Rather than "invalid request", a remediation says which capability to call, which field to change, or which document explains the rule that was violated. This exists because the alternative — an agent retrying the same call with slightly different guesses — is both expensive and unlikely to converge. A remediation turns a dead end into a next action. Where a remediation points at documentation, it names a document in this set. ## Status codes The usual meanings apply, with a few worth calling out: | Status | What it means here | |---|---| | `401` | The credential is missing, malformed or expired. Indistinguishable from a revoked one, deliberately. | | `403` | Authenticated, but this credential is not permitted this action or this scope. | | `404` | Either it does not exist, or it exists outside your scope. These are not distinguished, because distinguishing them leaks. | | `409` | A conflict with current state — something changed underneath you. Usually worth re-reading and retrying once. | | `422` | The request was understood and refused. The `detail` says why; a retry without changing something will fail identically. | | `429` | Rate limit reached. Back off and retry after the `Retry-After` header. | The `type` is an identifier to match on, not a page to fetch. A connected agent's daily budget is different from a rate limit: when it is spent, the tool call comes back as a result marked as an error whose text says the allowance is used up for the day — repeating the call will not get past it, and neither will backing off. ## Idempotency Write requests accept an idempotency key, and for some callers one is required. This is not decoration: a program that times out re-sends without ever learning whether the first attempt landed, and no operator is watching to notice the second one. Send a stable key per logical operation, and a retry is safe. A repeated key returns the original outcome rather than performing the action twice. ## Retrying Retry `409` once after re-reading state, and `429` after backing off. A spent daily budget is not retryable until the next day. Do not retry `422` unchanged — it is a considered refusal, not a transient fault. Do not retry a write without an idempotency key. --- --- title: "Expressions" summary: "Conditions in LatticeKit — who qualifies for a discount, which bookings a rule covers, when a workflow branches — are written in a small expression language, validated when saved and evaluated fail-safe at runtime." --- # Expressions Several things in LatticeKit are conditions rather than settings: which customers an audience includes, whether a discount applies to a cart, which bookings an access rule covers, whether a workflow takes one branch or another. These are written as expressions in a small, safe subset of a standard expression language. It has comparison and boolean operators, arithmetic, string and list operations, membership tests, and field access on the record being evaluated. It has no loops, no function definitions, no I/O, and no way to reach anything outside the value it was handed. ## Validated when written, not when run An expression is parsed and checked at the moment it is saved. A malformed one is rejected there — in front of the person writing it — rather than throwing at two in the morning inside a batch job. This is the main reason conditions are a language rather than an accreted pile of special-case flags. Configuration that can express logic eventually needs to; the only question is whether that logic is checkable. ## Fail-safe at runtime If an expression somehow still errors when evaluated — because the record it was handed lacks a field it expected, say — the result is treated as **false**, not true. That means a broken rule denies rather than grants: a discount does not apply, an audience does not include, an access rule does not admit. Failing open would turn a typo into free entry. ## What an expression can see Each place an expression is used hands it a defined set of fields, and it can see nothing else. An audience expression sees customer attributes; a cart discount expression sees the cart; an access rule sees the booking and the person. The available fields are part of the contract for that feature, and referring to a field that is not offered is a validation error at save time. ## Practical notes **Keep them short.** An expression that needs a comment is usually two rules. **They are evaluated frequently.** An audience expression may run against every customer; an availability rule against every slot. They are cheap by design, but complexity is still multiplied. **The same expression means the same thing everywhere.** The language is implemented identically on the server and in the browser-side renderer that previews tenant-facing pages, so a rule that previews one way does not behave another way in production. --- --- title: "Glossary" summary: "The vocabulary a caller meets in LatticeKit's APIs and documentation, defined in one place — the terms whose meanings are specific to this platform rather than generic." --- # Glossary Terms whose meaning here is specific rather than generic. Broad orientation is in [what LatticeKit is](what-is-latticekit.md). **Access rule** — a condition deciding whether a person may do something: enter a door, book a particular offer, use a particular resource. Written as an [expression](expressions.md) and evaluated fail-safe. **Attendance** — the record that a person actually turned up for a booking, recorded per person rather than per booking. What makes class registers and no-show policies work. **Audience** — a set of people defined by a condition rather than by a list, so membership is recomputed rather than maintained. **Availability** — when an [offer](#offer) can be taken. Comes in five shapes: grid, range, queue, open capacity, and external. See [scheduling](scheduling.md). **Booking** — one reservation of an offer, moving through a lifecycle from pending to confirmed to completed or cancelled. **Bundle** — a whole business's configuration as one portable document. See [tenant as code](tenant-as-code.md). **Capability** — one named, typed action the platform can perform. What is exposed to a given caller is declared rather than inferred, and hidden by default. **Consent** — a person's recorded permission to be contacted a particular way. Communications are checked against it and refused when it is absent, rather than sent and apologised for. **Grant** — the record an AI client's access hangs off: which business, which scopes, which location, whether sandboxed. Revoking it stops the client. See [agent API](agent-api.md). **Group** — the universal "this person belongs to something" model. A household, a membership tier, a staff roster and a team are all groups. **Hold** — an exclusive, expiring claim on a resource. What makes concurrent booking safe: two customers racing for the last court produce one booking and one clear failure. **Idempotency key** — a caller-supplied value making a retried write safe. A repeated key returns the original outcome rather than acting twice. See [errors](errors.md). **Obligation** — something a booking owes before it can confirm: a deposit, a signed waiver, a completed form. Tracked separately so a booking can say what it is waiting on. **Offer** — something a customer can book or buy, plus its terms: duration, requirements, deposit, cancellation policy. **Order** — a live tab or cart. Opens, accumulates lines, is fired, and closes — and closing is what produces the invoice, the ledger entries and the receipt. **Plan** — the difference between a bundle and what is live, produced without writing anything, and pinnable so that what was approved is what gets applied. **Problem document** — the machine-readable shape every error takes. See [errors](errors.md). **Proposal** — a consequential action an agent has asked for but cannot perform. Waits for a human decision. **Remediation** — a structured hint on a failure naming the concrete next step, so a program has something to do other than guess. **Resource** — a physical thing that can be committed: a court, a room, a chair, a unit of stock. **Scenario** — a run against the real configuration with every side effect neutralised, returning a structured verdict. How a configuration change proves itself. **Scheme** — the recurring shape of a week plus its exceptions, from which grid availability is generated. **Sandbox** — a posture in which configuration is real but side effects are neutralised and captured. Can be forced by a grant, and cannot be switched off by a request. **Stored value** — a balance a customer holds: a gift card, an account credit, a loyalty wallet. Moves through the ledger like anything else. **Sub-tenant** — a location or business unit within a business. See [tenancy](tenancy.md). **Tenant** — one business. The boundary nothing crosses. --- --- title: "Identifiers" summary: "Every LatticeKit id is a short type prefix followed by a time-ordered unique suffix, which makes ids self-describing, safe to log, and safe to pass between parts of the platform as opaque strings." --- # Identifiers Every record in LatticeKit has an id that looks like this: ``` bk3Qm8kFvR2nWxYt7pLc4Hs9 ``` Two parts, with no separator between them: a short prefix naming what kind of thing it is (two to four letters), then a unique, time-ordered suffix of 22 letters and digits. Ids are 24 to 26 characters depending on the prefix, always strings, always case-sensitive. ## Why the prefix is there **An id tells you what it is.** `bk…` is a booking, `inv…` an invoice, `prsn…` a person. A log line, a support ticket or an error message containing an id is self-explaining — nobody has to look it up to know what they are holding. **A wrong id fails safely.** Passing a booking id where an offer id belongs is simply not found — the same `404` an id from another business gets — never something else that happened to share a number. Some endpoints check the prefix at the route and refuse a malformed id outright; either way you get a not-found, not a wrong record. **Ids are safe to pass around.** They reveal nothing about storage, ordering, or how many of something exist. A sequential integer tells a competitor your booking volume; these do not. ## Why the suffix is time-ordered The unique part is generated so that ids created later sort after ids created earlier. That is not something to rely on for business logic — it is a database property. It means ids written at the same time land near each other in an index instead of scattering, which keeps large tables fast as they grow. Do not use id ordering to determine when something happened. Records carry explicit timestamps for that. ## Rules for callers **Treat ids as opaque.** Do not parse them, construct them, or derive one from another. The prefix is documented so you can recognise a type, not so you can build an id. **Do not assume an id is a foreign key.** Many references between parts of the platform are plain strings with no database relationship behind them — see [tenancy](tenancy.md) and [what LatticeKit is](what-is-latticekit.md). An id that resolves today is not guaranteed to resolve after the thing it names is deleted. **Compare exactly.** Ids are case-sensitive. Do not trim, lowercase or normalise them, and do not assume one length — the prefix decides it. **Store them as strings.** They are not numbers and will not fit in an integer column. ## Prefixes you will meet often | Prefix | What it names | |---|---| | `prsn` | A person — customer, member or staff | | `bk` | A booking | | `offr` | An offer — something bookable or purchasable | | `avlb` | An availability — when an offer can be taken | | `inv` | An invoice | | `ord` | An order | | `tn` | A tenant | There are well over a hundred prefixes in use. The reliable way to learn one is to read it off a record you already have, not to guess it. --- --- title: "Scheduling" summary: "How LatticeKit models what can be booked, when it can be booked, and the lifecycle a booking moves through — including the five shapes of availability and what can block a confirmation." --- # Scheduling Scheduling is the centre of most LatticeKit businesses, and it is built to cover more than appointments on a grid. Three concepts carry it: an **offer** is what can be taken, an **availability** says when it can be taken, and a **booking** is one instance of somebody taking it. ## Offer An offer is the sellable unit and the terms attached to it — how long it lasts, what it requires, whether it takes a deposit, and what happens if it is cancelled or changed. An offer's duration is not always a fixed number. It may be fixed (a 60-minute court booking), variable within bounds (a service that runs as long as it runs), open-ended, or multi-day. The cancellation and modification policy travels with the offer, so the rules are attached to the thing being sold rather than applied afterwards. ## Availability Availability answers "when can this actually be taken", and it comes in five shapes because businesses genuinely differ: - **Grid** — discrete slots at regular times. Courts on the hour. - **Range** — any start time within a window, for a duration the customer chooses. - **Queue** — no fixed time; customers take a place and are served in order. Walk-ins. - **Open capacity** — a number of places rather than a set of times. Sixteen seats in a class, whoever wants them. - **External** — the truth lives in another system and LatticeKit reads it. A single offer can have more than one availability. A class might be bookable in advance on a grid and also walk-up on a queue. Grid availability is generated from a **scheme** — a description of the recurring shape of a week, plus exceptions for holidays and closures. Changing the scheme regenerates the slots rather than requiring each one to be edited. ## Booking A booking moves through a lifecycle. The important transition is from held to confirmed, because that is where the rules apply. ``` PENDING ──► CONFIRMED ──► COMPLETED │ │ └──► EXPIRED └──► CANCELLED ``` A booking of an offer that takes no deposit and needs no approval is **confirmed** the moment it is made. Any other booking starts **pending**: the place is held, but not yet earned. A pending booking has an expiry — if nothing resolves it, the hold is released and the place returns to the pool. This is what stops abandoned checkouts from silently consuming inventory. The exception is a booking the customer has already paid a deposit for, on an offer where the business approves each booking: once it is paid it no longer expires, and it waits for the business to confirm or cancel it. A pending booking becomes **confirmed** when everything it owes has been satisfied. Where an offer takes a deposit, that means the deposit is paid. Where the business approves each booking itself, that means it has approved this one. Where it requires a signed waiver or a completed form, that means the form is in. These are tracked as obligations rather than as booking fields, so a booking can be waiting on more than one thing and can say which. **Completed** is reached by the passage of time and, where the business records it, attendance. **Cancelled** applies the offer's policy — which may mean a refund, a partial refund, a credit, or nothing. ## What can block a confirmation If a booking will not confirm, it is almost always one of: - An unresolved obligation — an unpaid deposit, an unsigned waiver, an unanswered form. - A resource that could not be committed. A booking that needs a court *and* a coach needs both; the platform allocates all-or-nothing rather than half-booking. - An access rule the customer does not satisfy — a membership tier requirement, or a condition the business wrote. - The slot being taken between the hold and the confirmation. The failure will say which. See [errors](errors.md). ## Resources Bookings that need physical things commit them through holds rather than by writing a field. A hold is exclusive and expires, which is what makes concurrent booking safe: two customers racing for the last court produce one booking and one clear failure, not two bookings. Resources come in kinds — a specific numbered thing, a quantity of interchangeable stock, or something the business defines itself. ## Group bookings A booking can cover a party rather than a person, and can carry a roster of who is actually attending. Attendance is recorded per person, which is what makes class registers, no-show policies and per-member entitlements work. --- --- title: "Tenancy and scope" summary: "How LatticeKit isolates one business from another, how locations and business units narrow that further, and what a caller needs to know about which scope its credentials carry." --- # Tenancy and scope Every piece of data in LatticeKit belongs to exactly one **tenant** — one business. Nothing crosses between tenants. This is the platform's strongest guarantee and the one that everything else assumes. ## Where the tenant comes from A caller never states which tenant it is acting for as an ordinary parameter. The tenant is carried by the credential — a verified token claim, or the grant an API key or connected agent resolves to. It cannot be overridden by a header or a request body. (A location *within* a business — where a business has several — follows the same rule for connected agents; how a first-party console session selects its location is described with the console.) This matters if you are integrating: **there is no "act on behalf of tenant X" parameter.** A credential is scoped to one business, and getting a second business means getting a second credential. ## Sub-tenants: locations and business units A business with more than one location can model each as a **sub-tenant**. A record either belongs to one sub-tenant, or is tenant-wide. The distinction is made per kind of record, not per row, so it stays consistent: - **Location-private** — a schedule, a booking, a resource. These belong to the place they happen, and a location does not see another's. - **Tenant-shareable** — a product, a customer, a price list. These normally belong to the business as a whole. A business also picks how strictly this applies. A **lenient** posture lets tenant-wide records be seen from inside a location, which is usually what people expect. A **strict** posture does not, and is the stronger isolation — it is deliberate, not a default. ## What a caller sees If your credential carries a sub-tenant scope, reads are filtered to it and writes are checked against it. You will not see other locations' data, and an attempt to write outside your scope is refused rather than silently redirected. If your credential has no sub-tenant scope, you see the business as a whole. This is enforced centrally rather than screen by screen, so it behaves identically whether the caller is a person in a console, an automated workflow, or a connected AI agent. For a connected agent there is no path with weaker rules. ## Practical consequences **A record's scope is set when it is created** and is not casually changed afterwards. Deciding scope before a second location opens is much cheaper than deciding it after. **Reports and realtime feeds are keyed by tenant and sub-tenant together**, so per-location figures are the default rather than something to filter for. **Cross-tenant queries do not exist.** If you need to compare two businesses you operate, you query each and combine the results yourself. --- --- title: "Tenant as code" summary: "A whole business exports as one portable YAML document that can be diffed against what is live, applied transactionally, and rolled back — so a configuration change is something you review rather than a sequence of writes you supervise." --- # Tenant as code A business's whole configuration — schedules, offers, resources, catalog, pricing, tiers, pages, workflows, promotions — exports as a single portable document called a **bundle**. A bundle can be compared against what is live, applied, and undone. ## Why this exists Configuring a business by making fifty ordered write calls is unreviewable and unrepeatable. A human has to supervise every call; a failure halfway leaves a half-built business nobody can describe; and nothing can be diffed or reverted. One document inverts all of that. It can be read before it is applied, applied atomically, compared with what came before, and re-applied to go back. It also collapses two things into one artefact: "what is this business?" and "make one like it" become the same file. ## Handles, not ids Every entry in a bundle carries a **handle** derived from its natural name, and every reference names a handle rather than an id. That is what makes a bundle portable — the same document applies to a different business unchanged. Where an id is unavoidably buried inside a payload the format does not own, it is rewritten to a symbolic reference. A final pass reports any raw id that survived, so a bundle that would not travel says so rather than looking clean. ## Configuration only The format is an **allowlist**: a field is in a bundle because someone deliberately added it. That means no people, no memberships, no bookings, no orders, no invoices, and no credentials — none of it can ride along, because none of it is named. This is the opposite of redacting. A redaction pass fails by leaking something nobody thought of; an allowlist fails by omitting a field, which is the safer direction. ## Plan, then apply Producing a **plan** compares a bundle against what is live and returns the minimal difference — what would be created, which fields would change, what would be removed — with hard-to-reverse changes flagged and explained. Planning writes nothing. A plan can be **pinned**, which stores it exactly as shown. Applying a pinned plan either executes that plan or refuses; it cannot quietly do something different because the business moved underneath it. That distinction is what makes review meaningful: the thing approved and the thing performed are the same object. Applying lands in a history with a digest, counts, who did it and which plan it came through — and an earlier document can be re-applied to roll back. ## Starters Ten working businesses ship as ordinary bundles: racquet club, gym, salon, studio, restaurant, barbershop, spa, clinic, hotel and music school. They are shaped differently on purpose, because the businesses are — one books resources by the hour, one runs a timetable needing a room *and* a trainer, one has two appointment lengths where the long one takes a deposit, and one is capacity-shaped rather than resource-shaped. Nothing about a starter is special once applied. It walks the same sections, produces the same history and rolls back the same way. Variables in a starter are a textual substitution, not a template language. An unset one is refused rather than left blank — a schedule anchored to an empty timezone is wrong in a way nobody notices until a booking lands at the wrong hour. ## Scenarios A **scenario** runs against the real configuration with every side effect neutralised and returns a structured verdict. Nothing is sent, charged or reserved. Each starter ships one as its acceptance test, which is what lets an agent prove its own work: apply a configuration, run the scenario, and get back whether the business it just built actually functions — something to book, an offer to book it under, a customer record to book it for. ## For agents On the agent surface, applying a bundle is a proposal rather than an action: an agent emits the document, and a human approves it once. See [the agent API](agent-api.md). --- --- title: "What LatticeKit is" summary: "Orientation for anyone — person or agent — working against a LatticeKit business for the first time: what the platform models, the handful of concepts everything else is built from, and what it deliberately does not do." --- # What LatticeKit is LatticeKit runs businesses that sell **time, things, and the combination of the two** — a racquet club renting courts by the hour, a gym running a class timetable, a salon booking appointments against named staff, a studio selling seats in a room. It handles scheduling, the catalog, customers, payments, the ledger, communications and reporting as one system rather than as integrations between five. It is also built to be operated by software. A business can be read, changed and verified through an API designed for AI agents as much as for applications — see [the agent API](agent-api.md). Not sure it fits? Describe your business at `/start` on the console and see it set up on LatticeKit before you sign up — what your customers would book, your hours, your menu. If it looks right, one click creates it and the setup assistant picks up from there. An AI agent can do the same on a person's behalf — describe, preview, build a live demo, then create the account that keeps it — through the signup endpoint of [the agent API](agent-api.md). ## The concepts everything else assumes Nine ideas carry most of the platform. Learn these and the rest is vocabulary. **Tenant.** One business. Every piece of data belongs to exactly one, and nothing crosses between them. A tenant may have **sub-tenants** — locations or business units — and rows can be scoped to one or shared across all. See [tenancy](tenancy.md). **Person.** A human the business knows: a customer, a member, a member of staff. People carry contact details, consents, and attributes the business defines. **Group.** The universal "this person belongs to something" model — a household, a membership tier, a team, a staff roster. Employment is a group; a tier is a group. **Resource.** A physical thing that can be committed: a court, a room, a chair, a piece of equipment, a unit of stock. Resources are held and released rather than simply booked, so two people cannot take the same one. **Offer.** Something a customer can buy or book, and the terms attached — how long it lasts, what it needs, whether it takes a deposit, what the cancellation policy is. **Availability.** When an offer can actually be taken. This is not always a grid of slots: it might be a range, a queue, a capacity, or something an external system owns. **Booking.** One reservation, moving through a lifecycle from held, to confirmed, to completed or cancelled. See [scheduling](scheduling.md). **Order.** A live tab or cart. It opens, things are added, it is fired, and it closes — and closing is what turns activity into money: an invoice, ledger entries, stock consumed, a receipt sent. **Invoice and ledger.** Money is recorded as double-entry bookkeeping, not as a status field. Every charge, refund, credit note and stored-value movement is a balanced posting, which is what makes a period closeable and a total trustworthy. ## Shapes of business it fits The scheduling model was built to cover more than appointments. It handles fixed grids (courts on the hour), variable durations (a service that takes as long as it takes), capacity (sixteen people fit in the room whether or not there are sixteen of anything else), queues and waitlists, multi-day stays, group bookings with rosters, and availability owned by an external system. If a business sells access to time or things, and needs the money and the communications that follow to be handled too, it is in scope. ## What it deliberately is not It is not a website builder, though it renders customer-facing booking pages. It is not a general accounting package, though it keeps a real ledger and exports to one. It is not a payment processor — it presents several as one interface and stores tokens, never card numbers. There are also verticals LatticeKit is not eligible to serve, for legal rather than technical reasons. Eligibility is checked when a business is set up, and an ineligible one is turned away rather than partially configured. ## Where to go next - [Glossary](glossary.md) — the vocabulary, in one place. - [Scheduling](scheduling.md) — offers, availability and the booking lifecycle. - [Identifiers](identifiers.md) and [tenancy](tenancy.md) — the two contracts that hold everywhere. - [Errors](errors.md) — how a failed call explains itself. - [Agent API](agent-api.md) — connecting an AI client. - [Tenant as code](tenant-as-code.md) — a whole business as one reviewable document. --- --- title: "Workflows" summary: "How a business automates its own operations in LatticeKit: actions built from typed steps, triggered by events, schedules or state, with three execution modes depending on whether the work must be immediate, backgrounded or durable." --- # Workflows A business can automate its own operations without writing code. An **action** is a sequence of steps the platform runs on the business's behalf: send a reminder the evening before a booking, apply a credit when a class is cancelled, notify a manager when stock runs low, follow up after a first visit. ## Actions and steps An action is a graph of typed **steps** rather than a script. Each step declares what it needs and what it produces, so an action can be validated before it ever runs, and a step that would receive something of the wrong shape is rejected at authoring time rather than at three in the morning. Steps cover the obvious operations — send a message, wait, branch on a condition, call a platform capability, update a record — and branching conditions are written as [expressions](expressions.md). Because steps call the same capabilities everything else calls, an action cannot do something an operator could not, and cannot do it under weaker rules. ## Triggers An action starts in one of three ways: - **Event** — something happened in the business. A booking was confirmed, an order closed, a payment failed. Event triggers can be filtered by an expression, so an action runs only for the cases that matter rather than for every event. - **Schedule** — a recurring time. Nightly, weekly, the first of the month. - **State** — a condition became true, evaluated on a cadence rather than in response to a single event. ## Execution modes Not all automation has the same reliability requirement, so an action declares which it needs: | Mode | For | |---|---| | **Immediate** | Short work whose result the caller waits for. | | **Background** | Work that should not block the caller, and that can be safely retried. | | **Durable** | Long-running or multi-step work that must survive restarts and continue exactly where it left off. | Durable is the one to choose when an action spans hours or days — a multi-stage follow-up sequence, a dunning cycle — because the platform keeps its position rather than starting over. ## Obligations Some workflows exist to resolve something a record is waiting on: a booking that cannot confirm until a deposit is paid or a waiver is signed. These are tracked as explicit obligations rather than as fields, so an action can satisfy one and the waiting record advances on its own. See [scheduling](scheduling.md). ## What to expect as an integrator Actions run asynchronously unless the mode says otherwise, so do not assume an effect has already happened when a call returns. Where the effect matters to you, read the record back or subscribe to the event rather than inferring it from a successful write.