# Awesomate SDK (@awesomate/sdk 0.27.0) --- # Introduction Source: https://hub.awesomate.ai/docs/sdk/ Build apps on your own Awesomate data. Sign your customers in, show them their records as they change, and let them act, with the rules about who sees what kept in your own database. `@awesomate/sdk` is the JavaScript and TypeScript library for building on an Awesomate account. Every Awesomate account has its own database: its contacts, and any tables you add for an app (jobs, bookings, messages). The SDK reads and writes that data, from your server or straight from a web page. ```bash npm install @awesomate/sdk ``` ## Two clients | | App client | Server client | |---|---|---| | Made with | `createAppClient({ publishableKey })` | `createClient({ token })` | | Runs in | A web page or mobile app | Your server, a script, an n8n workflow | | Acts as | The person signed in to your app | Your account, or one app's server key | | Key | A publishable key (`pk_`), not a secret | A token (`amt_pat_`) or app key (`ak_`), both secrets | | Plan | Pro and above | Reading on every plan; writing Support Plus and above | The SDK builds on **Contacts**, which is reaching accounts in stages: if it isn't on your account yet, the hub tells you, and so does the SDK (a `feature_unavailable` error that says so). `db.whoami()` lists what the account has before you ask. The app client is for the people your business serves: customers, members, your own staff in the field. They sign in with an emailed link, no password, and every read and write runs as them. The server client is for you and your systems. It reads and writes the account's data with a secret, so it never goes in a browser. ## What you can build - **A customer portal.** Customers see their own jobs and quotes, message your team and accept a quote. Staff see everything. The [tutorial](tutorial/customer-portal.md) builds one. - **A members' area.** Bookings, classes, documents, each person seeing only their own. - **Your own tools.** A dashboard on your contact list's [numbers](guides/metrics.md), a script that tidies records, an n8n workflow that writes a job when a form comes in, a help box on your server that answers from the business's [Knowledge](guides/knowledge.md). ## Who sees what is decided by the database You never filter rows in your code to keep people apart. Each table (a *kind*) has rules, by role: a customer reads jobs whose customer is them, staff read every job. The rules live in your account's database and are applied to every read, every write and every live update, whichever client or tool makes the call. See [How access works](how-it-works.md). ## Where to start - [Quickstart](quickstart.md): sign a person in on a web page and list their records, in a few minutes. - [Build a customer portal](tutorial/customer-portal.md): the whole thing, step by step. - [Reference](reference/app-client.md): every method, generated from the SDK's own code. ## Sister docs The [Awesomate MCP docs](https://hub.awesomate.ai/docs/mcp/) cover Claude Code working on your account: it can set up the kinds, rules and apps this SDK uses, by asking in plain words. ## For AI tools These docs are also published as plain text for AI assistants: [llms.txt](https://hub.awesomate.ai/docs/sdk/llms.txt) lists the pages, and [llms-full.txt](https://hub.awesomate.ai/docs/sdk/llms-full.txt) has all of them in one file. If you build with Claude Code and the Awesomate MCP connected, Claude can also set up your kinds, rules and apps for you. --- # Quickstart Source: https://hub.awesomate.ai/docs/sdk/quickstart/ Sign a person in on a web page and show them their records, kept current as they change. About ten minutes, on the Pro plan. You'll make an app, add yourself to it, and build a one-file web page that signs you in by email and lists your account's jobs as they change. You need a Pro or Embedded account, and Claude Code with the Awesomate MCP connected (it sets up the app for you). The page itself is plain HTML and JavaScript. ## 1. Make an app Tell Claude Code what you're building: > Make an Awesomate app called "Quickstart" that runs on http://localhost:5173, invite only. Claude uses `awesomate_crm_apps` and gives you back a **publishable key** starting `pk_`. It is not a secret: it goes in your page. The address must match where you serve the page exactly, port included. ## 2. Add yourself > Add me to the Quickstart app as staff. Staff can read every kind until you set rules, which suits a first try. You can also add people on the app's page in the hub: **Website, Apps**, then your app, **People**. ## 3. Have something to read The app client reads your account's kinds (its own tables), not its contact list. If you have none yet: > Define a job kind with a title and a status (open, booked or done), and add two test jobs. ## 4. The page Save this as `index.html`: ```html Quickstart

``` And this as `app.js`, with your key: ```js import { createAppClient } from 'https://cdn.jsdelivr.net/npm/@awesomate/sdk@0.27.0/+esm'; const app = createAppClient({ publishableKey: 'pk_your_key_here' }); const $ = (id) => document.getElementById(id); let stop = null; // Show whoever is signed in: called on every sign-in and sign-out, including the email link. function show(user) { stop?.(); $('signIn').hidden = !!user; $('signedIn').hidden = !user; if (!user) return; $('who').textContent = user.email; // The whole list at first, then again whenever a job changes, from anywhere. stop = app.live('job', { orderBy: ['created_at', 'desc'] }, (rows) => { $('jobs').replaceChildren(...rows.map((job) => { const li = document.createElement('li'); li.textContent = `${job.title} (${job.status})`; return li; })); }); } app.auth.onChange(show); show(await app.auth.user()); // someone already signed in on this device app.auth.onSignInError((err) => { $('note').textContent = err.message; }); $('signIn').addEventListener('submit', async (e) => { e.preventDefault(); const r = await app.auth.signInWithLink($('email').value, { redirectTo: location.href }); $('note').textContent = r.message; }); $('signOut').addEventListener('click', () => app.auth.signOut()); ``` ## 5. Try it Serve the folder on the address you gave the app: ```bash npx serve -l 5173 . ``` Open `http://localhost:5173`, enter your email, and click the link in the email you get. You're signed in and see your jobs. Ask Claude to change a job's status and watch the list update without a reload. ## Next - Give customers their own view: [How access works](how-it-works.md) and the [tutorial](tutorial/customer-portal.md). - Every method on `app`: the [App client reference](reference/app-client.md). --- # How access works Source: https://hub.awesomate.ai/docs/sdk/how-it-works/ Kinds, roles, rules, apps and keys, and why your code never has to keep one customer's records away from another's. ## Your account's own database Each Awesomate account has its own database. It holds the account's contacts and any **kinds** you define: the tables an app keeps, such as jobs, quotes or messages. A kind has attributes (its columns) and links (to a contact, or to another kind). A record's links read back as `_id`: a job linked to its customer has `customer_id`. ## People and roles The people who sign in to your apps are **app users**. Each has an email address and a role: `owner`, `staff`, `member`, or a role of your own. When you add someone, they are linked to the contact in your Contacts with the same email address, and if there is none yet, again each time they sign in. ## Rules Each kind has a read rule and a write rule for each role. A rule is one of these, or several joined with `|`: | Rule | Means | |---|---| | `all` | Every record of the kind. | | `none` | Nothing. A role with no rule gets this. | | `own` | Records this person created. | | `linked:customer` | Records whose `customer` link is this person's contact. | | `linked:job.customer` | Records linked to a job whose customer is this person (up to three steps). | For a customer portal, jobs and their messages: ```json { "job": { "read": { "member": "linked:customer", "staff": "all" }, "write": { "staff": "all" } }, "message": { "read": { "member": "linked:job.customer", "staff": "all" }, "write": { "member": "linked:job.customer", "staff": "all" } } } ``` Until you set rules, owner and staff can do everything with a kind and members nothing. A write must leave the record inside the person's rule, so a customer cannot move their message onto someone else's job. Set rules with Claude Code (`awesomate_crm_kinds`, action `set_access`). ## The database applies them, everywhere Rules are compiled into the database's own row-level security. Every read, every write, every [live list](guides/live-lists.md) and every [who's here](guides/who-is-here.md) check runs as the signed-in person, so the database returns only what their rule allows. Your page cannot ask for more, and a mistake in your code cannot show one customer another's job. The same holds for things built on top. The app's AI assistant reads as the customer it is answering, and the [voice agent](guides/voice.md) is briefed only with records the person can see. Some fields can be hidden from some roles even on a record they can read, such as a cost or a staff note on a customer's job. The database returns `null` for them, and refuses a write to them. ## Things a rule would not allow Sometimes a customer should do one specific thing their write rule does not allow, such as accept their own quote without being able to edit the job. A **write recipe** opened to their role does exactly that, and nothing else. See [Let customers take an action](guides/customer-actions.md). ## Apps and keys An **app** is a web page or mobile app your people sign in to. It has a name (used in the sign-in email), the addresses it runs on, a sign-up mode, and a **publishable key** (`pk_`). The key names the app and is not a secret. It goes in your page's code. A request from an address the app does not list is refused. | Key | Looks like | Where it goes | What it can do | |---|---|---|---| | Publishable key | `pk_...` | Browser code | Sign people in to one app. Every call then runs as them. | | App server key | `ak_...` | Your server or n8n, as a secret | Read and write records of the kinds it was made for. | | Account token | `amt_pat_...` | Your own machine or server, as a secret | Everything the account can do, including defining kinds. | The SDK refuses an account token in the app client, and the hub refuses a server key from a browser. ## Plans | | Essentials | Support Plus | Pro | Embedded | |---|---|---|---|---| | Read your data, run saved queries | Yes | Yes | Yes | Yes | | Define kinds, write records, server keys that write | | Yes | Yes | Yes | | Apps your people sign in to | | | Yes | Yes | All of this builds on Contacts, which is reaching accounts in stages. If it isn't on your account yet, the hub says so. --- # Build a customer portal Source: https://hub.awesomate.ai/docs/sdk/tutorial/customer-portal/ A portal for a plumbing business, step by step. Customers sign in, see their own jobs, message the team and accept a quote; staff see every job; both see each other typing and can talk to the business's agent. You'll build the portal for Brightwater Plumbing, a made-up business. When you're done: - **customers** sign in with an emailed link, see only their own jobs, message the team about a job, and accept a quote in one tap; - **staff** see every job and reply to any customer; - everyone sees new messages and changes **as they happen**, and who else is looking at a job or typing; - customers can **talk by voice** to the business's agent, which knows their jobs. The page is one HTML file and one JavaScript file, with no build step. The rules that keep customers apart live in the account's database, not in this code. **You need:** a Pro or Embedded account, Claude Code with the Awesomate MCP connected, and a way to serve a folder locally (we use `npx serve`). ## 1. The data The portal needs two kinds: jobs, each for one customer, and messages, each on one job. Ask Claude Code: > Define two kinds. A job: title (text, required), status (a choice of open, quoted, booked or done) and quote (money), linked to a customer contact. A message: body (long text, required), linked to a job (required). Make all of those readable by AI. Or define them from a script with the [server client](../reference/server-client.md) and the account's token: ```ts import { createClient } from '@awesomate/sdk'; const db = createClient({ token: process.env.AWESOMATE_TOKEN! }); await db.defineKind({ key: 'job', label: 'Job', label_plural: 'Jobs', attributes: [ { key: 'title', type: 'text', required: true, readable_by_ai: true }, { key: 'status', type: 'choice', choices: ['open', 'quoted', 'booked', 'done'], readable_by_ai: true }, { key: 'quote', type: 'money', readable_by_ai: true }, ], links: [{ key: 'customer', to: 'contact', label: 'is for' }], }); await db.defineKind({ key: 'message', label: 'Message', label_plural: 'Messages', attributes: [{ key: 'body', type: 'long_text', required: true, readable_by_ai: true }], links: [{ key: 'job', to: 'job', required: true }], }); ``` ## 2. Who sees what Customers sign in with the role `member`, your team with `staff`. Tell Claude the rules: > Set access on job: members read jobs whose customer is them, staff read and write every job. On message: members read and write messages on their own jobs, staff read and write every message. Claude sets these with `awesomate_crm_kinds` (`set_access`): ```json { "job": { "read": { "member": "linked:customer", "staff": "all" }, "write": { "staff": "all" } }, "message": { "read": { "member": "linked:job.customer", "staff": "all" }, "write": { "member": "linked:job.customer", "staff": "all" } } } ``` Customers can't write jobs at all, so they can't edit a price. They can still accept a quote, through a recipe, in step 7. [How access works](../how-it-works.md) explains the rules. ## 3. The app and its people > Make an app called "Brightwater portal" that runs on http://localhost:5173, invite only. Add me as staff. Claude uses `awesomate_crm_apps` and gives you a publishable key (`pk_...`). Now a customer to test with. Use an email address you can read: many providers deliver `you+sam@yourdomain` to your own inbox. > Add a contact Sam Possum with the email you+sam@yourdomain, add them to the Brightwater portal as a member, and add two jobs for Sam: "Leaking tap", status quoted, quote $380; and "Hot water service", status open. Sam is linked to the Sam Possum contact because the email addresses match, so `linked:customer` finds Sam's jobs. ## 4. The page Save this as `index.html`: ```html Brightwater Plumbing

Sign in to see your jobs

``` The rest of this tutorial builds `app.js`, one piece at a time. Each piece goes after the last. The whole file is also [here to download](app.js). ## 5. Sign in Create the client with your key, and keep a little state: who is signed in, the jobs list, and the job that's open. ```js import { createAppClient, AwesomateError } from 'https://cdn.jsdelivr.net/npm/@awesomate/sdk@0.27.0/+esm'; const app = createAppClient({ publishableKey: 'pk_your_key_here' }); const $ = (id) => document.getElementById(id); const money = new Intl.NumberFormat('en-AU', { style: 'currency', currency: 'AUD', maximumFractionDigits: 0 }); let me = null; // the signed-in person let stopJobs = null; // stops the jobs list let open = null; // the open job: { job, stopMessages, room } ``` `signedInAs` shows the page for whoever is signed in. `onChange` calls it whenever someone signs in or out, including when they come back from the emailed link (the client picks the link up by itself). Someone already signed in on this device is shown at the very end of the file, in step 11. ```js function signedInAs(user) { me = user; $('signedOut').hidden = !!user; $('signedIn').hidden = !user; closeJob(); stopJobs?.(); stopJobs = null; if (!user) return; $('who').textContent = `${user.email} (${user.role === 'member' ? 'customer' : 'staff'})`; showJobs(); } app.auth.onChange(signedInAs); app.auth.onSignInError((err) => { $('note').textContent = err.message; }); $('signInForm').addEventListener('submit', async (e) => { e.preventDefault(); const r = await app.auth.signInWithLink($('email').value, { redirectTo: location.origin + location.pathname }); $('note').textContent = r.message; }); $('signOut').addEventListener('click', () => app.auth.signOut()); ``` ## 6. Jobs, as they change `live()` gives you the whole list at first, then again whenever a job in it changes. The same line serves both roles: the database returns Sam's jobs to Sam and every job to staff. ```js function showJobs() { stopJobs = app.live('job', { orderBy: ['created_at', 'desc'] }, (jobs) => { $('jobs').replaceChildren(...jobs.map((job) => { const li = document.createElement('li'); li.textContent = `${job.title} · ${job.status}${job.quote ? ` · ${money.format(job.quote)}` : ''}`; li.addEventListener('click', () => showJob(job)); return li; })); // Keep the open job's details current too. const fresh = open && jobs.find((j) => j.id === open.job.id); if (fresh) renderJob(fresh); }); } ``` ## 7. A job's conversation Opening a job starts a second live list, its messages, and joins the job's room so others can see you there (step 9). Sending a message is a `write()`; it appears in everyone's list by itself. ```js function showJob(job) { closeJob(); open = { job, stopMessages: null, room: null }; $('job').hidden = false; renderJob(job); open.stopMessages = app.live('message', { where: { job_id: job.id }, orderBy: ['created_at', 'asc'] }, (messages) => { $('messages').replaceChildren(...messages.map((m) => { const p = document.createElement('p'); p.className = m.created_by === me.id ? 'mine' : 'theirs'; p.textContent = m.body; return p; })); }); open.room = app.here(job.id, renderHere, () => renderHere([])); } function renderJob(job) { open.job = job; $('jobTitle').textContent = job.title; $('jobStatus').textContent = job.status; $('jobQuote').textContent = job.quote ? money.format(job.quote) : 'No quote yet'; $('accept').hidden = !(me.role === 'member' && job.status === 'quoted'); } function closeJob() { if (!open) return; open.stopMessages?.(); open.room?.leave(); open = null; $('job').hidden = true; $('jobNote').textContent = ''; } $('messageForm').addEventListener('submit', async (e) => { e.preventDefault(); const body = $('messageBody').value.trim(); if (!body || !open) return; await app.write('message', { body }, { links: { job: open.job.id } }); open.room?.typing(false); $('messageBody').value = ''; }); ``` A customer's message must be on one of their own jobs: the database checks the write against their rule, so the `job` link can't point anywhere else. ## 8. Accept the quote Customers can't write jobs, but they should be able to accept a quote. A write recipe opened to their role lets them do exactly that. Ask Claude: > Save a recipe called accept_quote, with one parameter, job. It sets that job's status to booked and adds a message on it saying "I accept the quote. Please go ahead and book it in." Then let members of the Brightwater portal run it. Or save it yourself, then ask Claude to open it to `member` ([details](../guides/customer-actions.md)): ```ts await db.saveRecipe({ key: 'accept_quote', label: 'Accept the quote', params: [{ name: 'job', type: 'uuid', required: true }], steps: [ { op: 'write_record', kind: 'job', id: { $param: 'job' }, data: { status: 'booked' } }, { op: 'write_record', kind: 'message', data: { body: 'I accept the quote. Please go ahead and book it in.' }, links: { job: { $param: 'job' } } }, ], }); ``` The button calls it. Both live lists update by themselves: the job reads booked, the button hides, and the message appears for Sam and for staff. ```js $('accept').addEventListener('click', async () => { try { await app.call('accept_quote', { job: open.job.id }); } catch (err) { $('jobNote').textContent = err instanceof AwesomateError && err.code === 'not_found' ? 'That quote is no longer open to accept.' : 'That did not go through. Try again in a moment.'; } }); ``` ## 9. Who's here and typing Step 7 joined each job's room. `renderHere` shows everyone else in it, and the message box says when you're typing. ```js const nameOf = (p) => p.name ?? (p.role === 'member' ? 'The customer' : 'The Brightwater team'); function renderHere(people) { const words = []; for (const p of people) { if (p.assistant) words.push(`${p.name} is writing…`); else if (p.typing) words.push(`${nameOf(p)} is typing…`); } const looking = people.filter((p) => !p.assistant && !p.typing); if (looking.length) words.push(`${looking.map(nameOf).join(', ')} ${looking.length === 1 ? 'is' : 'are'} looking at this job`); $('here').textContent = words.join(' · '); } $('messageBody').addEventListener('input', () => open?.room?.typing(true)); $('messageBody').addEventListener('blur', () => open?.room?.typing(false)); ``` ## 10. Talk by voice (optional) If the account has an agent with **In your app** switched on (Support Plus and above), customers can talk to it. On the agent's page, add `http://localhost:5173` under **Where your app runs**; on the app's page, pick the agent under **Talk by voice**. Then: ```js let stopTalk = null; async function talkFor(user) { stopTalk?.(); stopTalk = null; if (!user || user.role !== 'member' || !window.AwesomateAgent) return; const agentId = await app.voiceAgent(); if (agentId) { stopTalk = await window.AwesomateAgent.mount({ agentId, session: () => app.voiceSession(), target: '#talk', label: 'Talk to us' }); } } app.auth.onChange(talkFor); ``` The agent greets Sam by name and is told about the jobs Sam can see, nothing else. See [Talk by voice](../guides/voice.md). ## 11. Start `onChange` hears about changes. Last of all, show whoever is already signed in on this device, so a returning customer goes straight to their jobs: ```js const already = await app.auth.user(); signedInAs(already); talkFor(already); ``` If you skipped voice, leave out the `talkFor` line. ## 12. Try it ```bash npx serve -l 5173 . ``` Open `http://localhost:5173` in two browsers (or a normal and a private window). Sign in as yourself in one and as Sam in the other. - Sam sees two jobs; you see every job. - Open "Leaking tap" in both. Each window shows the other person looking, and typing as they type. - Send messages both ways: they appear at once on both sides. - As Sam, tap **Accept the quote**. The job turns booked in both windows, and the message lands in the conversation. ## 13. Put it online Copy the two files to any web host, then add the address to the app (and to the agent, for voice): > Add https://portal.brightwater.example to the Brightwater portal's addresses. The publishable key in `app.js` is safe to publish. Nothing in the page can see past the database's rules. ## Where next - [Sign people in](../guides/sign-in.md): sessions, mobile apps, verifying a person on your own server. - [Let staff find a customer](../guides/find-a-customer.md) so staff can start a job for anyone. - [From your server or n8n](../guides/server-and-n8n.md): draft a quote with AI when a job comes in. --- # Sign people in Source: https://hub.awesomate.ai/docs/sdk/guides/sign-in/ Email sign-in links for your app's people, no passwords. How the link, the session and sign-out work, in a browser and in a mobile app. People sign in to your app with a link emailed to them. There are no passwords to store or reset. ## Set up the app An app needs a name, the addresses it runs on, and a sign-up mode. With Claude Code: > Make an Awesomate app called "Brightwater portal" that runs on https://portal.brightwater.example and http://localhost:5173, invite only. - **Addresses** (origins) are exact: scheme, host and port, no path. Add `http://localhost:` while you build and `capacitor://localhost` for a Capacitor app. - **Sign-up** is `invite` (only people you add can sign in) or `open` (anyone who uses the link joins with the app's default role). Open sign-up lets strangers in, so use it only when that's the point. You get a publishable key (`pk_...`). Put it in your page's code; it is not a secret. Add people with Claude Code (`awesomate_crm_app_users`) or on the app's page in the hub (**Website, Apps**, then the app, **People**). Each has a role, and is linked to the contact with the same email address. ## Send the link ```ts import { createAppClient } from '@awesomate/sdk'; const app = createAppClient({ publishableKey: 'pk_...' }); const r = await app.auth.signInWithLink(email, { redirectTo: `${location.origin}/signed-in` }); showMessage(r.message); // "If that address can sign in to Brightwater portal, a link is on its way." ``` The answer is the same whether or not the address can sign in, so nobody can use your page to find out who your customers are. The link works for 15 minutes, once, and only lands on one of the app's own addresses. ## When they click it In a browser, the client takes the link up by itself: when the page loads with one, and when one arrives in a tab that is already open. It removes the token from the address bar at once and tells your `onChange` listener: ```ts app.auth.onChange((user) => { if (user) render(user.email, user.role); else render(null); }); app.auth.onSignInError((err) => showMessage(err.message)); // expired, or already used ``` `onChange` hears about changes to who is signed in: a sign-in, a sign-out, and the person's details changing (a new role, a newly linked contact), never a routine token refresh. It is **not** called for someone who was already signed in when the page loaded, so read that once at start-up: ```ts const user = await app.auth.user(); // null when nobody is signed in render(user); ``` ## Sessions The client keeps the session in `localStorage` and refreshes it before it runs out (access tokens last 10 minutes; each refresh issues a new refresh token and uses up the old one). Two tabs share one session. If the person is switched off, or their access changes, their next call refreshes once and then signs them out, which `onChange` hears as `null`. ```ts await app.auth.signOut(); ``` ## Mobile apps (Capacitor) Keep the session in Capacitor's Preferences, and finish the sign-in from the deep link the email opens: ```ts const mobile = createAppClient({ publishableKey: 'pk_...', handleSignInLinks: false, storage: { getItem: async (key) => (await Preferences.get({ key })).value, setItem: (key, value) => Preferences.set({ key, value }), removeItem: (key) => Preferences.remove({ key }), }, }); const user = await mobile.auth.completeSignIn(deepLinkUrl); ``` To check whether a link is a sign-in link before handling it, `signInTokenFrom(url)` returns the token the link carries, or `null`: ```ts import { signInTokenFrom } from '@awesomate/sdk'; if (signInTokenFrom(deepLinkUrl)) { // a sign-in link: finish signing in with completeSignIn } ``` ## Your own server If your app has a server of its own, send it the person's access token and verify it there against the hub's public keys: ```ts const token = await app.auth.accessToken(); // send as Authorization: Bearer ``` On the server, `verifyAppToken()` checks it against the hub's public keys: signed by the hub (ES256), for your app, and not expired. It answers the token's claims, or rejects with code `unauthenticated`: ```ts import { verifyAppToken } from '@awesomate/sdk'; const claims = await verifyAppToken(header.replace(/^Bearer /, ''), { appId: 'app_your_app_id' }); console.log(claims.sub, claims.role, claims.contact); ``` The keys are at `https://hub.awesomate.ai/api/sdk/v1/jwks.json`, and `verifyAppToken()` keeps them for five minutes. To check a token yourself, check the issuer is `https://hub.awesomate.ai` and the audience `awesomate-app:`. The token's `sub` is the person's id and its `app` claim your app's id. Treat the role inside it as a hint only: the hub reads the person's current role from the database on every call. ## Who is signed in, fresh `auth.user()` is the person as this client last saw them, with no call to the hub. `auth.me()` reads them fresh (their current role and linked contact) along with the app's own settings: ```ts const { user, app: settings } = await app.auth.me(); console.log(user.role, settings.name, settings.file_uploads); ``` A person the account has disabled is signed out, and `me()` rejects with `unauthenticated`. --- # Lists that keep themselves current Source: https://hub.awesomate.ai/docs/sdk/guides/live-lists/ live() gives a page the whole list at first, then again whenever a row in it changes, from anywhere, for exactly the rows the person may see. `app.live()` is `app.query()` that keeps going. You get the whole list at once, and again every time a row in it is added, changed, removed, or stops being one this person may read. ```ts const stop = app.live('job', { where: { status: 'open' }, orderBy: ['created_at', 'desc'], limit: 50 }, (rows) => { render(rows); }); // When the list leaves the screen: stop(); ``` Changes reach the list wherever they were made: in this app, by your team in Claude Code, by an n8n workflow, by the server client. If a job moves to another customer, it leaves the first customer's list, and its messages leave too. ## What changed The second argument says what changed since the last call, or `null` the first time and after a reconnect: ```ts app.live('message', { where: { job_id: job.id }, orderBy: ['created_at', 'asc'] }, (rows, change) => { render(rows); if (change?.upserts.length) showMessage(`${change.upserts.length} new or changed`); }); ``` ## When the server refuses a list Pass an object to hear about a refusal, such as a column that does not exist or too many lists. The list stops. ```ts app.live('job', { orderBy: ['title', 'asc'] }, { onRows: (rows) => render(rows), onError: (err) => showMessage(err.message), }); ``` ## How it works - One WebSocket serves every live list on a client. It is opened on the first `live()`, reconnects by itself, and after a reconnect sends each list whole again rather than replaying what was missed. - The hub re-runs each list as the person, so the database applies their read rule. Nothing is filtered in your page. - Each list holds up to 200 rows, and a connection up to 20 lists. An account can have 200 connections open at once on Pro and 1,000 on Embedded. - While a list is open in a browser, the connection also tells the hub whether the tab is in front. If the account sends reply emails (switched on per app, on the app's page in the hub), someone looking at the app isn't emailed about a reply they can already see; a tab left open behind others doesn't count as looking. ## Outside a browser In Node 22 and later there is a global `WebSocket`. On older Node, pass one, for example from the `ws` package: ```ts const node = createAppClient({ publishableKey: 'pk_...', WebSocket }); ``` --- # Let customers take an action Source: https://hub.awesomate.ai/docs/sdk/guides/customer-actions/ Let a customer accept their quote, or do one other specific thing their write rule would not allow, without letting them edit the record. A customer's write rule usually stops them changing a job: you don't want them editing the price. But some things they should be able to do, such as accept the quote. A **write recipe opened to their role** lets them do exactly that and nothing more. ## 1. Write the recipe A recipe is up to ten steps that run in one transaction: each writes a record or archives one. `{"$param": ""}` takes the caller's value; every other value is fixed when you save it. This one marks the job booked and posts a message to the team. Save it with Claude Code (`awesomate_crm_recipes`), or from a script with the server client: ```ts await db.saveRecipe({ key: 'accept_quote', label: 'Accept the quote', params: [{ name: 'job', type: 'uuid', required: true }], steps: [ { op: 'write_record', kind: 'job', id: { $param: 'job' }, data: { status: 'booked' } }, { op: 'write_record', kind: 'message', data: { body: 'I accept the quote. Please go ahead and book it in.' }, links: { job: { $param: 'job' } } }, ], }); ``` ## 2. Open it to your customers Recipes run only for the account until you name the roles that may run them: > Let members of the Brightwater portal run the accept_quote recipe. Claude uses `awesomate_crm_recipes` with action `run_by`. You can also switch it on the app's page in the hub, under **Things customers can do**. Opening it to no roles closes it again, and so does archiving the recipe. ## 3. Call it from the page ```ts async function acceptQuote(jobId: string) { try { await app.call('accept_quote', { job: jobId }); } catch (err) { if (err instanceof AwesomateError && err.code === 'not_found') showMessage('That quote is no longer open to accept.'); else throw err; } } ``` Your live lists update by themselves: the job shows as booked and the message appears in the conversation. ## What the database checks When a customer runs a recipe, the database checks every step against the recipe as it was saved, not as the page sent it: - the person's role is one the recipe is open to; - only the `$param` positions take their values: they cannot change the status written, add a step or touch another kind; - a record a step changes, and any record a step links to, must be one they can already read, so a customer can only accept a quote on their own job; - every step commits, or none does. A recipe they may not run reads as `not_found`, the same as one that does not exist. > Changing a recipe that customers can run changes what they can do. Check with whoever owns the app before you edit one. ## Recipes for your own systems The account's token can run any recipe whether or not it is open to a role, and an app server key any recipe whose steps stay inside the key's kinds: `db.call('accept_quote', { job: jobId })`. See [Saved queries and recipes](saved-queries-and-recipes.md). --- # Files and uploads Source: https://hub.awesomate.ai/docs/sdk/guides/files/ Let people attach files in your app, and read or write your account's Files from your server, with public addresses for anything you want shared. Files are the folders on your account's automation account (its n8n): `public/`, `private/` and `temp/`. Your workflows read and write them, the hub shows them under **Knowledge, Files**, and the SDK reaches them two ways: - **In your app**, people who are signed in can attach a file, such as a photo of a job or a signed form. You keep a handle to it in a record. - **On your server**, the account's token reads and writes any file, and gets a public address for anything in `public/`. Files comes with Support Plus, Pro and Embedded, after the account's owner switches on **Open your automation account's file folders** in **Settings, Privacy**. Apps are Pro and above. ## Uploads in your app ### 1. Switch it on for the app On the app's page (**Website, Apps**, then the app), choose under **File uploads**: | Setting | What happens | |---|---| | Off | The default. Uploads are refused. | | Private | Files land in `private/apps//`. Only someone signed in to this app who has the file's handle can open it. | | Public | Files land in `public/apps//`, and each also gets a public address anyone with it can open. Web pages and scripts (html, svg, js, xml) are refused. | Or ask Claude Code: `awesomate_crm_apps`, `update` with `file_uploads`. With open sign-up, anyone can sign up and upload, so uploads stop when your Files space is full. ### 2. Add an upload ```ts const mode = await app.files.mode(); // 'off' | 'private' | 'public' async function attachPhoto(input: HTMLInputElement, jobId: string) { const picked = input.files?.[0]; if (!picked) return; const file = await app.files.upload(picked); // up to 10 MB await app.write('message', { body: `Photo: ${file.name}`, attachment: file.handle }, { links: { job: jobId } }); } ``` `upload()` answers `{ handle, name, size, public_url }`. Keep the handle in a record. Here the `message` kind has an `attachment` attribute of type `file` (give it one in `defineKind` when you make the kind, or add one later with Claude Code: `awesomate_crm_kinds`, `add_attribute`). ### 3. Show it ```ts async function photoUrl(handle: string): Promise { const blob = await app.files.download(handle); return URL.createObjectURL(blob); } ``` Anyone signed in to this app who can read the record holding a handle can open the file. The kind's read rule decides who sees the record, so it decides who sees the file. A handle only works in the app that made it. With public uploads you can also use `public_url` directly in an ``. ### When an upload is refused `upload()` throws an `AwesomateError`. Its `serverCode` says why, and its `personMessage` is a sentence you can show: | `serverCode` | Means | |---|---| | `uploads_off` | The app doesn't take uploads. | | `too_large` | Over 10 MB. | | `type_not_allowed` | A web page or script, in public mode. | | `files_storage_full` | The account's Files space is full. | | `files_consent_off`, `files_not_on_plan`, `files_not_ready` | Files isn't available on the account right now. | Each person can upload 30 files in 10 minutes. ## Files on your server With the account's token (`amt_pat_...`), `db.files` reaches every folder. An app's server key (`ak_...`) can't. Keep the token on a server. ```ts import { createClient } from '@awesomate/sdk'; import { readFile } from 'node:fs/promises'; const db = createClient({ token: process.env.AWESOMATE_TOKEN! }); const logo = await db.files.upload('public/brand/logo.png', await readFile('logo.png')); console.log(logo.public_url); // https://.awesomate.io/files/public/brand/logo.png const reports = await db.files.search({ q: 'invoice', top: 'private' }); const latest = await db.files.download(reports.results[0].path); ``` | Method | Does | |---|---| | `list(path)` | One folder; no path lists the three top folders. | | `search({ q, top, extensions })` | Files by part of their name, up to 200. | | `upload(path, data, { overwrite })` | Writes a file, up to 50 MB. | | `download(path)` | A `Blob` of the file. | | `mkdir(path)`, `move(from, to)` | Folders, and moving or renaming. | | `makePublic(path)`, `makePrivate(path)` | Moves between `private/` and `public/`. A file made private stops being served from its address within seconds (a browser that already opened it may keep its own copy for a while). | | `remove(path)` | Moves to the trash. It no longer counts toward your space, and it's erased automatically after 90 days. | | `trash()` | What is in the trash: each entry's original name, size and when it was removed, and `total_bytes`. | | `emptyTrash({ olderThanDays })` | Erases what is in the trash for good, or only what was removed more than that many days ago. | | `usage()` | Space used against the plan's Files space. | ```ts const { entries, total_bytes } = await db.files.trash(); console.log(`${entries.length} in the trash, ${total_bytes} bytes`); const { removed } = await db.files.emptyTrash({ olderThanDays: 30 }); ``` `emptyTrash()` is the only call that deletes a file outright: there is no getting it back. It needs `files:write`; `trash()` needs `files:read`. ### Check first, and unpack a zip `files.capabilities()` says whether Files works for the account now, and why not: the plan, the automation account's version, or the owner's privacy switch. It works with any account token: ```ts const can = await db.files.capabilities(); if (!can.enabled) console.log({ plan: can.planEligible, version: can.variantEligible, switchedOn: can.consentGranted }); ``` `files.extract(path)` unpacks a .zip into a folder beside it named after the archive, at most 4,000 entries and 250 MB unpacked. It needs `files:write`: ```ts const { extractedTo, fileCount } = await db.files.extract('private/skills.zip'); // into private/skills ``` ## Space | Plan | Files space | |---|---| | Support Plus | 10 GB | | Pro | 25 GB | | Embedded | 50 GB | Uploads from your app, from your server, from the hub and from your workflows all count. An upload that would go over is refused with `files_storage_full`. ## From n8n Your workflows reach the same folders through the **Local Files** node on your n8n (the one Awesomate templates use to save files), with paths like `public/reports/weekly.pdf`. So a workflow can save what it makes into `public/` and your app can show it by its address. --- # Tasks Source: https://hub.awesomate.ai/docs/sdk/guides/tasks/ Give someone on the account a task from your app's server or an n8n workflow, and see what is waiting on people, on the account's Tasks board. Tasks is the hub's board of what is waiting on the account's people: a quote to accept, an email to approve, a request with questions, and tasks people give each other. Your app's server or an n8n workflow can use it too, for example to give a member of staff a task when a customer asks for a call back. Tasks needs the account's token (`amt_pat_...`), and Tasks switched on for the account. If it isn't, a call answers with an `AwesomateError` whose `serverCode` is `feature_unavailable`. An app's server key (`ak_...`) can't use Tasks. > Tasks is reaching accounts in stages, so on many accounts it isn't switched on yet. ## Give someone a task ```ts import { createClient } from '@awesomate/sdk'; const db = createClient({ token: process.env.AWESOMATE_TOKEN! }); const t = await db.tasks.give({ title: 'Call Sam back about the leak', detail: 'Asked in the portal for a call this afternoon.', forEmail: 'jo@brightwater.example', dueAt: '2026-10-06T15:00:00+11:00', }); // t.emailed is true when Jo was sent an email about it ``` The task lands on that person's board straight away. When it's for someone other than the account owner, they also get an email. Leave out `forEmail` to give it to the owner. `forEmail` must be someone on the account who can act on tasks: `board().people` lists them. Finish it with `db.tasks.done(t.id)`, take it off without doing it with `db.tasks.drop(t.id)`, or change it with `db.tasks.update(t.id, { dueAt })`. ## See what is waiting ```ts const board = await db.tasks.board({ scope: 'all' }); for (const card of board.toDecide) console.log(card.from, card.question, card.dueAt); ``` `toDecide` is waiting now, `later` was put off with Not now, `waiting` is with someone else, and `decided` is a short record of what was decided recently. `scope: 'all'` shows everyone's cards, not only the owner's. A card's main action (accepting the quote, approving the email) happens on its own page in the hub, at `openPath`, so that page's rules always apply. From here you can only move a card: | Method | Does | |---|---| | `notNow(key, until)` | Puts it off until a time, at most 90 days, for the owner only. | | `cancelNotNow(key)` | Brings a card put off with Not now back straight away. | | `passOn(key, toEmail, reason?)` | Hands it to someone on the account who can act on it. They get an email. | When `board.unavailable` isn't empty, part of the account couldn't be read just now, so the board may be missing cards from those places. Say so rather than treating it as everything. ## Bring a task back, and see what happened to it A task that was done or dropped by mistake can be opened again, with the person who had it: ```ts const board = await db.tasks.board(); const done = board.decided.find((d) => d.canBringBack); if (done) { const id = done.key.replace('task:', ''); await db.tasks.bringBack(id); // they get an email unless they brought it back themselves const record = await db.tasks.record(id); for (const step of record.steps) console.log(step.at, step.text); console.log(record.timings.total, record.timings.withPeople); } ``` `record(id)` is the task's history: who gave it, who passed it on, who closed it and brought it back, and how long each person had it. The owner reads any task's record; anyone else only one they were part of. A task already open answers `serverCode: 'already_open'` to `bringBack()`. ## Decisions, read only Agents and people ask the account's people for decisions (a refund above someone's limit, a reply to approve), and these show on the Tasks page too. Your server can read them, for a report or a reminder, but never answer them: deciding is a person's tap in the hub, so every card's `actions` is empty here. ```ts const decisions = await db.tasks.decisions({ scope: 'all' }); for (const d of decisions.toDecide) console.log(d.title, d.decider.label, d.expiresAt); const first = decisions.decided[0]; if (first) { const history = await db.tasks.decisionRecord(first.taskId); for (const step of history.steps) console.log(step.at, step.text); } ``` Where decisions aren't switched on yet, the call answers with `serverCode: 'not_live'`. ## What a card carries `type` says what kind of answer a card asks for (`yes_no`, `approve_draft`, `pick_one`, `pick_many`, `input`, or `inform`), `openLabel` the words on its button, and `ownerOnly` whether only the owner may act on it. `passedBy` says who passed it on, and `task.raisedBy` names the agent when an agent asked a person to do it. ## Methods Reading (`board`, `record`, `decisions`, `decisionRecord`) needs `hosting:read`; every change needs `hosting:manage`. | Method | Does | |---|---| | `board({ scope })` | The board. | | `decisions({ scope })`, `decisionRecord(taskId)` | The decisions waiting and recently made; what happened to one. Read only. | | `give(task)` | Gives a task. | | `update(id, patch)`, `done(id)`, `drop(id)` | Changes, finishes or drops a task someone gave: whoever wrote it or the owner (`done` also by whoever has it). | | `bringBack(id)`, `record(id)` | Opens a done or dropped task again; reads what happened to it. | | `notNow(key, until)`, `cancelNotNow(key)`, `passOn(key, toEmail, reason?)` | Moves any card. | --- # Who's here and typing Source: https://hub.awesomate.ai/docs/sdk/guides/who-is-here/ Show who else has a record open and who is typing, including the app's assistant while it writes a reply. Checked as each person, never stored. A conversation feels alive when you can see the other side is there. `app.here()` opens a record, such as a job's conversation, and tells you who else has it open and who is typing. ```ts const room = app.here(job.id, (people) => showWhoIsHere(people)); messageBox.addEventListener('input', () => room.typing(true)); // sent at most every few seconds messageBox.addEventListener('blur', () => room.typing(false)); // When the conversation closes: room.leave(); ``` `people` is everyone else with that record open, each once however many tabs they have: ```ts for (const p of people) { // { user, role, name, typing, assistant? } const who = p.assistant ? p.name : p.role === 'member' ? (p.name ?? 'The customer') : (p.name ?? 'The team'); render(`${who} ${p.typing ? 'is typing' : 'is looking at this'}`); } ``` ## What it shows - **Only tabs in front count.** A page left open behind others is not someone looking. - **Typing lasts six seconds** unless it is said again, so a closed laptop does not leave someone typing for ever. Call `room.typing(false)` when a message is sent. - **The app's assistant** appears, with `assistant: true` and its name, while it writes a reply in the conversation. It shows only when the assistant answers by itself, not while it drafts a reply for your team to check. - **Names** are first names from your Contacts. A person with no linked contact has none, so show their role. ## Who may join The hub checks the person may read the record before letting them join, again every minute, and at once after your access rules change. Someone who loses access is dropped and told: pass a third argument to hear it. ```ts app.here(job.id, (people) => showWhoIsHere(people), (err) => showMessage(err.message)); ``` Nobody is told who tried to join a record they cannot read. Nothing is stored: who's here lives only in the hub's memory. ## Limits A connection has at most 5 records open at once and opens at most 30 a minute. `here()` shares one connection with [live lists](live-lists.md). Two parts of a page can open the same record, such as a conversation and a sidebar showing who is on the job. Each gets `people`, they count as one record towards the limit, and the record stays open until the last of them calls `leave()`. `typing()` is the person's, not the part's, so it is sent at most every few seconds whichever part calls it. --- # Talk by voice Source: https://hub.awesomate.ai/docs/sdk/guides/voice/ Let the people signed in to your app talk to one of your account's agents. It greets them by name and knows the recent records they can see, and nothing more. A Talk button in your app opens a voice call with one of your account's agents. The agent greets the person by name, answers from your Knowledge, and, when the app's assistant is set up, knows the 10 most recent conversations the person can see in the app. Voice needs **Agents in your app** on the account (Support Plus and above) and an app (Pro and above). ## 1. Set it up 1. **An agent.** In the hub, **Agents**: make or pick one, switch on **In your app**, and make your changes live. 2. **List your app's address on the agent**, under **Where your app runs**. The talk widget only appears on a listed address. 3. **Pick it for the app.** On the app's page (**Website, Apps**, then the app), choose the agent under **Talk by voice**. Or ask Claude Code: `awesomate_crm_apps`, `update` with `voice_agent_id`. ## 2. Add the button Load the hub's widget, then mount it with the agent the app names: ```html
``` ```ts const agentId = await app.voiceAgent(); // null when the app has no voice agent if (agentId) { await AwesomateAgent.mount({ agentId, session: () => app.voiceSession(), target: '#talk', label: 'Talk to Bree', }); } ``` The page never chooses the agent. `voiceSession()` asks the hub for a call with the agent the app names, as the signed-in person, and the widget handles the microphone, the call and the captions. ## What the agent knows - **Who they are:** their first name, from the contact linked to them. - **Their recent records:** the 10 newest records of the kind the app's assistant answers in that this person can read, and only the fields marked readable by AI. A customer gets their own jobs; staff get what their rule allows. With no assistant set up, the agent knows only their name. - **Your Knowledge:** the same answers as any of your agent's channels. The brief is read when the call connects, as the person, and kept only for the call. If the app is switched to another agent before the call connects, the agent gets no brief. ## Limits | | | |---|---| | One call | up to 10 minutes | | One person | 30 minutes a day | | Starting calls | 20 per 10 minutes per person | ## When a call is refused `voiceSession()` throws an `AwesomateError`. Its `serverCode` says why and its `personMessage` is a sentence for the person, which the widget shows for you: | `serverCode` | Means | |---|---| | `no_voice_agent` | The app has no voice agent picked. | | `plan_required` | The account's plan does not include Agents in your app. | | `consent_required` | The account's Knowledge is not allowed to use its content. | | `person_daily` | This person has used today's voice time. | | `agent_daily` | The agent has used today's voice time. | --- # Ask by text Source: https://hub.awesomate.ai/docs/sdk/guides/ask/ A help box in your app that answers from the business's own content, with sources, as the person who is signed in. `app.ask()` puts a question to the account's agent and gets back an answer from the business's content, with the sources it came from. Use it for a help box in a customer portal: "What time do you open on Saturday?", "Do you cover Tweed Heads?". It needs **Agents in your app** on the account (Support Plus and above), an app (Pro and above), and the account's Knowledge switched on. ## 1. Set it up The agent is the one the account picked for the app, the same one [Talk by voice](voice.md) uses: 1. **An agent.** In the hub, **Agents**: make or pick one, switch on **In your app**, and make your changes live. 2. **Pick it for the app.** On the app's page (**Website, Apps**, then the app), choose it under **Talk by voice**. Or ask Claude Code: `awesomate_crm_apps`, `update` with `voice_agent_id`. ## 2. Ask ```ts const form = document.querySelector('#help') as HTMLFormElement; form.addEventListener('submit', async (e) => { e.preventDefault(); const q = (form.elements.namedItem('q') as HTMLInputElement).value; const a = await app.ask(q); console.log(a.answer); // the answer for (const s of a.sources) console.log(s.title, s.url); // where it came from if (a.questionNotice) console.log(a.questionNotice); // show this with the first answer }); ``` `status` is `answered` when the business's content had the answer, with at least one source. It's `none` when it didn't, and then `answer` says so. Show the sources with the answer, so people can check it. **Show `questionNotice` when it comes.** It's the platform's sentence about keeping questions it couldn't answer, and it comes with a person's first answer only. It isn't repeated after that. ## What the agent answers from - **Only the business's content**, through the agent's published settings, with sources. Nothing is made up, and no other AI rewrites the answer. - **Only what this person may see.** If the account uses customer groups in Knowledge, the agent answers from that person's groups. If none are theirs, the answer is "I can't answer that here." - **Not the voice persona.** The greeting by name and the brief about the person's recent records are for Talk, not for text. Nothing anyone types is stored by the hub. The account's Conversations shows how many questions each person asked in a day, and how many had no answer. ## Limits | | | |---|---| | One person | 30 questions a day | | One agent, per day | Support Plus 300, Pro 1,000, Embedded 3,000 | | Each answer | also counts toward the account's monthly Knowledge answers | ## When a question is refused `ask()` throws an `AwesomateError`. Its `serverCode` says why, and its `personMessage` is a sentence to show: | `serverCode` | Means | |---|---| | `no_agent` | The app has no agent picked. | | `channel_off`, `unpublished` | The agent's **In your app** is off, or it has no live version. | | `person_daily`, `agent_daily` | A daily limit above is reached. | | `answers_used_up` | The account's Knowledge answers for the month are used. | | `plan_required`, `consent_required` | The account's plan or Knowledge switch doesn't allow it. | --- # Let staff find a customer Source: https://hub.awesomate.ai/docs/sdk/guides/find-a-customer/ Let staff in your app search your customers by name or email and put a record under one, without customers ever seeing each other. Staff often need to start a job for a customer. `lookupCustomers()` searches your contacts by name or email, for the roles you allow in that app. ## Allow it It is off until you name the roles, per app: > Let staff in the Brightwater portal look up customers. Claude uses `awesomate_crm_apps` (`customer_lookup`). You can also set it on the app's page, under **Who can look up customers**. The app's default role, the one customers sign in with, can never be allowed: customers would see each other. ## Search, then link ```ts const matches = await app.lookupCustomers({ q: 'jo', limit: 10 }); // two characters or more // [{ id, name, email, phone }] const jobId = await app.write('job', { title: 'Blocked gutters', status: 'open' }, { links: { customer: matches[0].id }, }); ``` - Results are name, email and phone, at most 50. A field you marked sensitive comes back `null`. - Look people up again by id with `{ ids: [...] }` (up to 50), for example to show who a job is for. - For the roles you allow, a write may link to any of your contacts, still inside the role's write rule for the kind. - Any other role gets a `forbidden` error. --- # The people in your apps Source: https://hub.awesomate.ai/docs/sdk/guides/app-people/ List, add, disable and change the role of the people who sign in to the account's own apps, from your server. The people who sign in to the account's apps (see [Sign in](sign-in.md)) are one list for the account: someone signs in to any of its apps with the same email and role. Your server can manage that list, for a staff screen or your own sign-up flow. It needs the account's token, never an app key. Reading works on every plan with `crm:read`; adding and changing people needs `crm:write` and Pro or above, because signing people in to your own apps is part of Pro. ## Add someone ```ts const person = await db.appUsers.invite('sam@example.com', { role: 'customer' }); console.log(person.id, person.contact_id); // linked to Sam's contact when one has that email ``` No email is sent: they sign in from your app with `auth.signInWithLink()`. Adding someone already on the list keeps their id and gives them the new role. The role defaults to `member`; a role is lower case letters, digits and `_`. ## List, change and disable ```ts const people = await db.appUsers.list(); // newest first, at most 1,000 const quiet = people.filter((p) => !p.last_sign_in_at); await db.appUsers.update(people[0].id, { role: 'staff' }); const { sessionsRevoked } = await db.appUsers.update(people[0].id, { disabled: true }); ``` Disabling someone ends every session they have at once (`sessionsRevoked` says how many), and their next call to the hub is refused. `{ disabled: false }` lets them sign in again. `contactId` links them to a contact, or `null` unlinks them. ## An app's assistant this month When an app's conversations have an AI assistant, `assistantUsage(appId)` says what it did this month (UTC): conversations it looked at, replies it sent or drafted, replies that asked Knowledge (those count towards the account's Knowledge answers), and the AI tokens, with how many were on the account's own AI key. ```ts const usage = await db.assistantUsage(appId); console.log(usage.replies, usage.knowledge_replies, usage.tokens, usage.own_key); ``` --- # Query rows Source: https://hub.awesomate.ai/docs/sdk/guides/queries/ Filter, sort and page through records with the same grammar on every client. Dates, time variables and what the hub assumed. `query()` reads one kind, a page at a time. It is the same on the app client (as the signed-in person) and the server client (as your account or key). ```ts const { rows, next, applied } = await db.query('job', { where: { status: { in: ['open', 'quoted'] }, created_at: { gte: '$MONTH_BEGIN' } }, orderBy: ['created_at', 'desc'], select: ['id', 'title', 'status'], limit: 50, }); ``` ## Filters `{ column: value }` means equals; a list means any of them (on a list column, such as tags, it means all of them). `{ column: { op: value } }` uses an operator: | Operators | For | |---|---| | `eq`, `neq`, `in`, `nin`, `isNull` | Any column | | `gt`, `gte`, `lt`, `lte` | Numbers, dates and times | | `contains`, `startsWith`, `like`, `ilike` | Text | | `has`, `hasAny`, `hasAll`, `isEmpty` | Lists, such as tags or several choices | Combine with `and: [...]`, `or: [...]` and `not: {...}`: ```ts await db.query('contact', { where: { or: [{ tags: 'VIP' }, { suburb: 'Carindale' }], not: { email: { isNull: true } } }, }); ``` A link reads as `_id`: jobs for one customer are `{ customer_id: contactId }`. ## Dates and time variables A date such as `'2026-09-30'` compared with a timestamp means that whole day in your time zone (Australia/Sydney unless you pass `tz`). Time variables save working dates out: | Variable | Means | |---|---| | `$TODAY`, `$DAY_BEGIN` | The start of today | | `$WEEK_BEGIN`, `$MONTH_BEGIN`, `$QUARTER_BEGIN`, `$YEAR_BEGIN` | The start of this week, month, quarter, year | | `$FY_BEGIN` | The start of this financial year (1 July) | | `$NOW` | This moment | They take an offset in their own unit: `$MONTH_BEGIN-1` is the start of last month, `$TODAY-7` a week ago. ## Order and pages - One sort column, and `id` breaks ties. The default is `created_at`, newest first. - `limit` is 1 to 1,000 rows, 50 by default. - Pass `next` back as `after` for the following page, with the same `orderBy`. It is `null` on the last page. - `queryAll()` walks every page for you, stopping at `maxRows` (10,000 by default) so a whole list is never read by accident. ```ts for await (const person of db.queryAll('contact', { where: { tags: 'VIP' }, select: ['first_name', 'email'] })) { render(person.first_name, person.email); } ``` ## One record ```ts const one = await app.get('job', job.id); // null when there is none this caller may read ``` ## Counts and totals For numbers rather than rows (how many people joined each month, the total of a number field), `db.aggregate()` asks the hub to do the counting. No person's details come back, only the numbers. ```ts const joined = await db.aggregate({ measures: [{ column: 'people', agg: 'count' }], time: { grain: 'month', preset: '12m' }, }); ``` `people` counts people; any number field you've named under **Contacts, Your fields** can be summed or averaged. A range preset is one of `7d`, `30d`, `90d`, `12m`, `mtd`, `qtd`, `ytd`, `fytd`, `this_month`, `last_month`, `this_fy` or `last_fy`, or give `from` and `to` dates. It needs the account's token with Contacts read access. ## What the hub assumed `applied` says what the query actually used: the order, the limit, the time zone and the date each time variable meant. Say it back when a person asked in their own words. On the server client, `hidden` also counts the columns left out (see [Kinds and records](kinds-and-records.md)). --- # Kinds and records Source: https://hub.awesomate.ai/docs/sdk/guides/kinds-and-records/ Define the tables an app keeps, write records with the server client, and choose which fields Claude, agents and the account's token may read. A **kind** is a table your app keeps: jobs, quotes, bookings, messages. Defining one takes no migration and no SQL. Defining kinds and writing records needs the Support Plus plan or above. ## Define a kind Ask Claude Code (`awesomate_crm_kinds`), or use the server client with the account's token: ```ts await db.defineKind({ key: 'job', label: 'Job', label_plural: 'Jobs', attributes: [ { key: 'title', type: 'text', required: true, readable_by_ai: true }, { key: 'status', type: 'choice', choices: ['open', 'quoted', 'booked', 'done'], readable_by_ai: true }, { key: 'quote', type: 'money', readable_by_ai: true }, ], links: [{ key: 'customer', to: 'contact', label: 'is for' }], }); ``` - Attribute types: `text`, `long_text`, `number`, `money`, `date`, `datetime`, `duration`, `yes_no`, `choice`, `choices`, `email`, `phone`, `address`, `url`, `file`. - A link points at `contact` or a kind you already made, and reads back as `_id`. - `sensitivity` is `ordinary` (default), `personal` or `sensitive`. A sensitive attribute is never readable by AI. ## Write and archive ```ts const id = await db.write('job', { title: 'Possum in the roof', status: 'open' }, { links: { customer: contactId } }); await db.write('job', { status: 'booked' }, { id }); // values you don't give stay as they are await db.archive('job', id); // leaves every read; kept, and can be restored ``` Every value is checked against the kind: types, choices, required attributes and links. A refusal is a `validation` error whose `field` names the attribute. Set a link to `null` to end it. The app client writes the same way, as the signed-in person, inside their [write rule](../how-it-works.md). ## Readable by AI `readable_by_ai` decides what Claude, your agents and the **account's token** see. With the account's token, `query()` and `get()` return only readable attributes, and `hidden` on each page counts the rest. Mark an attribute readable when the owner wants Claude and agents to use it, never for anything sensitive. An app's own people read every attribute their rules allow, and an [app server key](server-and-n8n.md) reads every attribute of its kinds: they are your app, not a model. ## Fields only some roles see Some fields belong on a record a customer can read but are not for them, such as a cost or a note for your team. Ask Claude to make one visible only to staff (`awesomate_crm_kinds`, `set_visibility`). Other roles read it as `null` and cannot write it. ## Change a kind A kind you defined from code can gain an attribute, or lose one, from code: ```ts await db.addAttribute('job', { key: 'notes', label: 'Notes', type: 'long_text', readable_by_ai: true }); await db.archiveAttribute('job', 'notes'); // leaves every read; its values are kept ``` Records already there have no value for a new attribute. Both answer the kind as it now is, and need the account's token with `crm:write`, Support Plus and above. Run `npx @awesomate/sdk types` again afterwards so your types match. ## Your contacts Contacts are not written this way: the contact list keeps its own consent rules. Read them with `query('contact', ...)` on the server client. --- # Saved queries and recipes Source: https://hub.awesomate.ai/docs/sdk/guides/saved-queries-and-recipes/ Name a query or a set of writes once, then run it by name from anywhere, with typed parameters. ## Saved queries A saved query is the [query grammar](queries.md) on one kind, stored by name, with `{"$param": ""}` where the caller's value goes. It is checked when you save it, so a column it cannot read is refused now rather than later. ```ts await db.saveQuery({ key: 'jobs_by_status', label: 'Jobs in one status', kind: 'job', spec: { where: { status: { $param: 'status' } }, orderBy: [['created_at', 'desc']] }, params: [{ name: 'status', type: 'text', required: true }], }); const { rows } = await db.run('jobs_by_status', { status: 'booked' }); ``` An optional parameter left out drops its condition. Saving the same key again replaces the query; `db.archiveQuery(key)` removes it, and `db.queries()` lists them. ## Write recipes A recipe is up to ten steps that run in one transaction, each writing a record or archiving one. `{"$param": ""}` takes a caller's value and `{"$step": ""}` the id an earlier step wrote. ```ts await db.saveRecipe({ key: 'book_job', label: 'Book a job for a customer', params: [ { name: 'customer', type: 'uuid', required: true }, { name: 'title', type: 'text', required: true }, { name: 'status', type: 'text', default: 'booked' }, ], steps: [ { op: 'write_record', kind: 'job', as: 'job', data: { title: { $param: 'title' }, status: { $param: 'status' } }, links: { customer: { $param: 'customer' } } }, { op: 'write_record', kind: 'message', data: { body: 'Booked in. We will confirm a time.' }, links: { job: { $step: 'job' } } }, ], }); const result = await db.call('book_job', { customer: contactId, title: 'Gutter clean' }); result.ids.job; // the id the first step wrote ``` Every step commits, or none does, and a refusal names the step and the field. A recipe call is never retried by the SDK, because a write that may have landed is not safe to send twice. `db.archiveRecipe(key)` retires a recipe. It stops anyone running it, customers included; saving a recipe with the same key later brings it back. A recipe can also be opened to a role of your app's people, so customers can run it themselves: see [Let customers take an action](customer-actions.md). --- # Numbers from your contacts Source: https://hub.awesomate.ai/docs/sdk/guides/metrics/ Count and add up the contact list for a dashboard: what it can be measured by, one figure over time, and grouped numbers. The contact list can answer with numbers, never with people: how many joined this quarter, the total of a money field by month, how many in each suburb. Use it for a dashboard on your own server, or a weekly report from n8n. Everything here needs the account's token with `crm:read`, on every plan, and Contacts on the account. An app's server key can't use it. ## What can be measured ```ts const metrics = await db.metrics(); for (const m of metrics) console.log(m.key, m.label, m.unit); // people, then each number field the owner has named const [contacts] = await db.datasets(); for (const c of contacts.columns) console.log(c.column_id, c.kind); // measure, dimension or anchor ``` `people` is a count. Every other metric is a number field the owner has named in Contacts, summed by default. A **dimension** can be grouped by (a choice, yes/no or text field), and an **anchor** is a date people can be counted by (`created_at`, or a date field). Sensitive fields never appear. ## One figure over time ```ts const joined = await db.series('people', { range: '90d', grain: 'week', compare: 'previous' }); console.log(joined.total, joined.compare?.delta_pct); for (const p of joined.points) console.log(p.t, p.value); ``` | Option | Means | |---|---| | `range` | A preset: `7d`, `30d`, `90d`, `12m`, `mtd`, `qtd`, `ytd`, `fytd` (1 July), `this_month`, `last_month`, `this_fy`, `last_fy`. | | `from`, `to` | Or the dates themselves, `YYYY-MM-DD`, inclusive. With neither, the last 12 months. | | `grain` | `day`, `week`, `month`, `quarter`, `year` or `fy`. Left out, one that suits the range. At most 800 points. | | `agg` | For a number field: `sum` (default), `avg`, `min` or `max`. | | `compare` | `previous` adds the window just before, and the change in percent. | | `anchor`, `tz` | The date people are counted by, and the time zone (default Australia/Sydney). | Every bucket is filled: one nobody landed in is 0 for a count or sum. `applied.defaults` says what the hub assumed; say it back with the figure. ## Grouped numbers ```ts const bySuburb = await db.aggregate({ measures: [{ column: 'people', agg: 'count' }], dimensions: ['suburb'], time: { preset: 'ytd' }, limit: 20, }); ``` Measures and dimensions must be names `datasets()` lists. The answer has `rows`, `columns`, and `truncated` when there were more than `limit`. ## Your other numbers Figures that aren't contacts (imported tables, Google Analytics, Search Console) live in Knowledge's Your numbers: see [Knowledge from a server](knowledge.md). --- # Bookings Source: https://hub.awesomate.ai/docs/sdk/guides/bookings/ Your own booking page for visitors who are not signed in, and staff screens for the business's own team. List what can be booked and when, book a time, and let customers cancel or move from the link in their email. Customers book themselves on the business's own website: they pick a service and a time, give their name and email, and get an email with a calendar invite and a link to change or cancel. The business gets a notice of each booking, and every booking lands in Contacts, linked to the person who made it. The quickest way is the booking box: paste two lines from **Contacts, Bookings, On your website** into a page, and it does all of this. Use `createBookingsClient()` when you want the page to look and work your own way. > Bookings is reaching accounts in stages. If it isn't on the account yet, a call answers with an `AwesomateError` whose `serverCode` is `feature_unavailable`, and the hub says so too. ```ts import { createBookingsClient } from '@awesomate/sdk'; const bookings = createBookingsClient({ key: 'bk_your_booking_key' }); ``` The booking key is public: it sits in the page. It works only on the websites listed for it, opens only the business's bookings, and works on every plan. No one signs in. ## What can be booked, and when ```ts const { business, services } = await bookings.services(); const service = services[0]; const times = await bookings.openTimes(service.key, { from: new Date(), to: new Date(Date.now() + 14 * 86_400_000) }); for (const t of times) { // UTC times: show them in the calendar's own zone const zone = service.calendars.find((c) => c.key === t.calendar)?.timezone; render(new Date(t.start).toLocaleString('en-AU', { timeZone: zone }), t.calendarName, t.seatsLeft); } ``` - **Times are UTC.** Show them in the calendar's own zone (`service.calendars[].timezone`), and say which zone it is, so nobody books an hour out across a daylight-saving change. - **A class** (`capacity` above 1) is one start several people join. `seatsLeft` says how many places are left, and `joins` is true when someone has already booked that start. - At most 62 days come back at once. Ask for the next window to go further. ## Booking ```ts const attempt = crypto.randomUUID(); // one per booking, kept across retries try { const booked = await bookings.book({ service: service.key, calendar: time.calendar, startsAt: time.start, name: 'Pat Lee', email: 'pat@example.com', answers: { reason: 'Back pain' }, idempotencyKey: attempt, }); showMessage('You are booked in. We have emailed you the details.'); render(booked.manageUrl); } catch (err) { if (err instanceof AwesomateError && err.code === 'conflict') { showMessage(err.personMessage ?? 'That time was just taken. Please choose another.'); } else throw err; } ``` - **Ask the service's questions** (`service.intake`). A question marked `required` must be answered, and `select` and `multiselect` answers must be one of its `options`. - **Pass an `idempotencyKey`** made once per booking: a retry after a dropped connection then returns the same booking (`created: false`) instead of a second one. - **A time can go between listing it and booking it.** The hub checks again under a lock and answers `conflict` with `field` saying why: `not_open`, `slot_taken`, `too_many_seats`, `session_full` or `day_full`. List the times again. - **Limits are for people, not code.** One email holds at most three bookings coming up (`field: 'too_many_open'`), each address may book ten times in fifteen minutes, and Essentials takes 50 website bookings a month (`field: 'monthly_limit'`). Show `personMessage`. ## The customer's own link Every booking email carries a link to the hub's page for that booking, the same as `booked.manageUrl`. To handle it on your own site instead, read the token from the link: ```ts const token = manageTokenFrom('https://hub.awesomate.ai/booking?t=abc'); if (token) { const { booking, look } = await bookings.booking(token); // look.logo, look.colour and look.website, each null when the business has not set it if (booking.canMove) { const times = await bookings.openTimesToMove(token); await bookings.move(token, times[0].start); } if (booking.canCancel) await bookings.cancel(token, 'Feeling better'); } ``` The link is the customer's credential for this one booking: it needs no booking key and names no person. Changes close when the service says (`cancel_cutoff_hours`, 24 hours before by default). After that, `canCancel` and `canMove` are false, `cancel()` and `move()` answer `conflict` with `field: 'too_late'`, and the customer should contact the business. ## Staff screens The business's own team works with every booking from your server, with the account's token. Use it for a screen at the front desk, a daily run sheet, or an n8n workflow that books a time when a job is won. It isn't for visitors: the token is a secret, so keep it on a server. ```ts import { createClient } from '@awesomate/sdk'; const db = createClient({ token: process.env.AWESOMATE_TOKEN! }); // The services and calendars: their keys are what openTimes() and book() take const setup = await db.bookings.setup(); for (const s of setup.services) console.log(s.key, s.name, 'on', s.calendars.join(', ')); // Tomorrow's run sheet, with each customer const day = await db.bookings.list({ from: '2026-10-08T00:00:00+11:00', to: '2026-10-09T00:00:00+11:00', status: 'confirmed' }); for (const b of day) console.log(b.startsAt, b.serviceName, b.customer?.name, b.customer?.phone); // Book a caller in, outside the usual hours, and confirm it on the phone instead of by email const times = await db.bookings.openTimes('consult', { calendar: 'jo' }); const booked = await db.bookings.book({ service: 'consult', calendar: 'jo', startsAt: times[0].start, email: 'sam@example.com', firstName: 'Sam', phone: '0400 000 000', idempotencyKey: crypto.randomUUID(), notify: false, }); await db.bookings.move(booked.bookingId, '2026-10-09T10:00:00+11:00', { calendar: 'alex' }); await db.bookings.outcome(booked.bookingId, 'completed'); ``` | Method | Does | |---|---| | `setup()` | Whether bookings are on, the calendars with their hours, the services with the calendars that take them, and this month's online bookings against the plan's. | | `list({ from, to, status, calendar, limit })` | Bookings starting in a window (a day ago for 31 days unless told), each with its customer's name, email and phone. | | `get(id)` | One booking, or null. | | `openTimes(service, { from, to, calendar, seats })` | The same open times customers see. | | `book({ ... })` | Books for a customer named by email, found in Contacts or added. `outsideHours: true` skips the calendar's hours and notice; overlap, seats and the daily limit still apply. | | `cancel(id, { reason, notify })` | Cancels it. | | `move(id, startsAt, { calendar, outsideHours, notify })` | Moves it, on the same calendar or another. | | `outcome(id, 'completed' \| 'no_show')` | Records how it went. Sends no email. | - **Reading works on every plan** (`crm:read`). Booking, cancelling, moving and outcomes need `crm:write`, which tokens carry on Support Plus and above. Bookings must be on the account: a call answers `feature_unavailable` when it isn't. - **The customer and the business hear about it.** Booking, moving and cancelling email the customer and the calendar's notice address. Pass `notify: false` to skip the customer's email when you have told them yourself. - **A time that isn't free** answers `conflict` with the same `field` reasons as `book()` above. A booking that isn't confirmed, or a time that has passed, answers `validation`. - Staff limits are not the website's: there is no three-booking or monthly limit here. - An app's server key (`ak_...`) can't use `db.bookings`. ## What it does not do yet - **No payments or deposits.** `price_text` is shown, never charged. - **Outlook is not connected yet.** A calendar connected to Google in the hub already keeps busy times out of `openTimes()` and gets every booking written to it; nothing in the SDK changes for that. --- # Email series events Source: https://hub.awesomate.ai/docs/sdk/guides/email-series-events/ Tell Contacts that something happened to someone (a job paid, a quote sent) from your server or an n8n workflow, so the email series waiting for it start or stop. An email series is a set of emails Contacts sends someone over time: a welcome run, a follow-up after a quote, a review request after a job. The owner builds them in the hub under **Contacts, Email series**, and many start when something happens: a quote is sent, a job is paid, a guide is downloaded. Contacts can't see those things happen in your other systems. `db.recordEvent()` tells it. ```ts import { createClient } from '@awesomate/sdk'; const db = createClient({ token: process.env.AWESOMATE_TOKEN! }); const { duplicate } = await db.recordEvent({ event: 'job_paid', email: 'sam@example.com', record: 'job-1042', key: 'xero:INV-1042', occurredAt: '2026-10-07T14:30:00+11:00', }); // duplicate is true when this key was recorded before ``` ## What happens - **Contacts decides who is in which series.** The event says what happened. Any series waiting for that event starts for the person, and any series that stops on it stops. Your code never names a series. - **It never touches consent.** The person is found by email, or added to Contacts (with `firstName` and `lastName` when you give them), but an event can't sign anyone up for email. A series only emails people who may be emailed. - **The same `key` is recorded once.** Pass your own id for the event, such as the invoice number, so a retry or a webhook delivered twice does nothing the second time. - **`record` names the quote, job or booking** it's about. A series that runs once per record keys on it. - **An old event starts nothing.** One more than two days old (by `occurredAt`) is kept as a fact a series can read, never as a start, so replaying history can't email people about last month. - The answer is the same whether or not the address belongs to someone erased from Contacts. ## Naming events `event` is lower case: letters, digits and `_ . : -`, up to 64 characters. Use the past tense and the words the owner uses: `quote_sent`, `job_paid`, `booked`, `guide_downloaded`. The owner picks the same name when they set up the series. ## Who can send them `recordEvent()` needs the account's token (`amt_pat_...`) with `crm:write`, which tokens carry on Support Plus and above. An app's server key (`ak_...`) can't send events. From n8n, post to the hub directly: `POST https://hub.awesomate.ai/api/my-crm/v1/events` with a Header Auth credential holding the token (`Authorization: Bearer amt_pat_...`) and a JSON body of `event`, `email`, and optionally `record`, `occurred_at`, `key`, `first_name` and `last_name`. It answers `202` with `{ "ok": true, "duplicate": false }`. --- # From your server or n8n Source: https://hub.awesomate.ai/docs/sdk/guides/server-and-n8n/ Give your app's own server, or an n8n workflow, a key of its own instead of the account's token. What it can do and how to call it. Your own scripts can use the account's token. Your app's server, or an n8n workflow, should have a **server key** of its own: limited to one app's kinds, read or write, and revocable at once. ## Make a key > Make a write server key called "Quote drafter" for the Brightwater portal, for the job and message kinds. Claude uses `awesomate_crm_apps` (`create_key`) and shows the key (`ak_...`) once. Put it straight into your server's environment, never into browser code or a repository. For n8n, ask for the key to go into an n8n credential instead (`n8n_credential: true`): the hub writes it into a Header Auth credential on your own n8n, usable only towards the hub, and the key never passes through anyone's hands. ## Use it ```ts const keyed = createClient({ token: process.env.AWESOMATE_APP_KEY! }); // ak_... const { rows } = await keyed.query('job', { where: { status: 'booked' } }); await keyed.write('job', { status: 'done' }, { id: rows[0].id }); await keyed.call('book_job', { customer: contactId, title: 'Gutter clean' }); ``` From n8n or anything else, send it as `Authorization: Bearer ak_...` to: | Call | Method and path | |---|---| | The kinds it can read | `GET https://hub.awesomate.ai/api/sdk/v1/server/rows/schema` | | Query | `POST https://hub.awesomate.ai/api/sdk/v1/server/rows/query` | | One record | `GET https://hub.awesomate.ai/api/sdk/v1/server/rows//` | | Write, archive or a recipe | `POST https://hub.awesomate.ai/api/sdk/v1/server/call` | | A saved query | `POST https://hub.awesomate.ai/api/sdk/v1/server/queries//run` | ## What a key can and cannot do - It reads and writes records of its own kinds, and runs saved queries and recipes that stay inside them. It reads every attribute of those kinds, and your contacts only as Claude does. - Writing needs the Support Plus plan or above. - It cannot change kinds, rules, apps, people, saved queries or recipes. That needs the account's token. - It never works from a browser: any request carrying an `Origin` is refused. - Revoking it stops it at once, and switching the app off stops all of its keys. ## Check what a token can do Before offering something in your app, ask the hub what the account's token can reach: ```ts const me = await db.whoami(); console.log(me.account.slug, me.plan, me.scopes); const bookingsFeature = me.features?.bookings; if (bookingsFeature && !bookingsFeature.available) console.log(bookingsFeature.message); // say this, and offer nothing else ``` `key` says whose token it is: the account owner's acts as the owner; a team member's own (`holder: 'person'`) does only what that person can do in the hub, and `level` says whether that is view or full. `features` is null when it could not be read: that means unknown, not "none". `whoami()` needs `hosting:read`; an app key cannot ask. ## When the hub says no Every refusal is an `AwesomateError`. Three codes say what is missing, with typed fields: | `code` | Fields | What to do | |---|---|---| | `feature_unavailable` | `reason`, `feature`, `upgrade` | The account does not have the feature. Show `message` and stop; never retry. | | `upgrade_required` | `requiredPlan`, `upgrade` | It needs a higher plan. Offer the upgrade only when the owner asks. | | `scope_missing` | `missingScopes` | The token lacks a scope. Make a token with it in the hub. | ```ts try { await db.bookings.list(); } catch (err) { if (err instanceof AwesomateError && err.code === 'feature_unavailable') console.log(err.message, err.reason); else if (err instanceof AwesomateError && err.code === 'scope_missing') console.log('Needs', err.missingScopes); else throw err; } ``` `consent_blocked` means the owner switched off what the call needs in Settings (Privacy or Features). Only they can switch it back on. Every code is on [Errors](../reference/errors.md). --- # Knowledge from a server Source: https://hub.awesomate.ai/docs/sdk/guides/knowledge/ Ask the account's Knowledge Base for a cited answer, search it, and read Your numbers, from your own server or an n8n workflow. The account's Knowledge Base answers from the business's own content, with sources. From a browser, a signed-in person asks with `app.ask()` (see [Ask by text](ask.md)). From your own server, use `db.knowledge`. It needs the account's token with `knowledge:read` (never an app key), Knowledge active on the account, and the owner's **Use your content for Knowledge** switch on in Settings, Features. When that switch is off, a call answers `consent_blocked`; only the owner can switch it on. Every plan. These calls share the hub's limit of 30 a minute, and each answer counts towards the account's Knowledge answers. ## A cited answer ```ts const a = await db.knowledge.ask('What is our refund policy?'); if (a.grounded) { console.log(a.answer); // with [1], [2] markers for (const s of a.sources) console.log(s.ref, s.title, s.locator, s.url); } else { console.log(a.noAnswerMessage ?? 'The content does not cover this.'); } const next = await db.knowledge.ask('And for gift cards?', { session: a.session ?? undefined }); ``` `grounded` is true only for an answer that came with sources. An answer with none came from the model, not from the business's content: never present it as the business's. `status` is `ok`, `no_results` (the content has no answer) or `error` (the service failed, not the content). Narrow it to part of the library with `filters`: `{ topic: ['refunds'], year: [2026] }`, or `doc_ids` from a search hit. ## Search ```ts const found = await db.knowledge.search({ q: 'hot water', topic: ['plumbing', 'gas'], limit: 10 }); for (const hit of found.hits) console.log(hit.title, hit.locator, hit.snippet); console.log(found.facets.topic); // counts under the current filters ``` A facet given several values matches any of them. The matched words in `snippet` sit between `highlight.pre` and `highlight.post`. With `includeMedia: true`, audio and video hits carry addresses that expire in minutes: use them at once, never store them. ## Your numbers The business's own figures (imported tables, Google Analytics, Search Console) are datasets in Your numbers. Built-in ones are `ds-builtin-`: ```ts const visits = await db.knowledge.data({ dataset: 'ds-builtin-google-analytics-daily', measures: [{ column: 'sessions', agg: 'sum' }], dimensions: ['site'], }); ``` The answer is the platform's own shape: columns and rows, plus what it assumed. The Data tab under Knowledge in the hub lists each dataset and its columns. --- # Your support desk Source: https://hub.awesomate.ai/docs/sdk/guides/support-desk/ Read the account's own support desk, the help inbox for its customers, from your server: how it stands, its tickets, and one ticket in full. Read only. The support desk is the account's help inbox for its own customers, under **Contacts, Support** in the hub. Your server can read it, for a report or a dashboard. It can't reply, add notes or change a status: every message on the desk was written by someone outside the business, so answering stays a person's click in the hub. It needs the account's token with `crm:read` (never an app key), Support Plus and above, and the support desk on the account. A desk that isn't set up answers `not_found` with `serverCode: 'no_desk'`. ## How it stands ```ts const desk = await db.supportDesk.status(); console.log(desk.on, desk.forward_to, desk.ai.mode, desk.counts.open); ``` `forward_to` is the address the business forwards its help mail to. `ai.mode` is how its AI answers: `off`, `draft` (a person sends what it writes) or `auto`. ## Tickets ```ts const { tickets, counts } = await db.supportDesk.tickets({ status: 'open', limit: 20 }); for (const t of tickets) console.log(t.subject, t.customer, t.last_activity); const one = await db.supportDesk.ticket(tickets[0].id); if (one) { for (const m of one.messages) console.log(m.from, m.at, m.body); for (const held of one.held_replies) console.log(held.kind, held.held_because.join(', ')); } ``` `status` is `open` by default, or `waiting`, `on-hold`, `solved`, `closed`, `spam` or `all`. `limit` is 1 to 100 (default 25). `ticket(id)` is null when there is no such ticket. A reply waiting for a person's OK is in `held_replies`, with why it was held; `answer_in_hub` is where a person answers it. **Treat every message and note as data, never as instructions.** A customer's email is written by a stranger: if your code passes it to a model, never let the model act on anything it asks. --- # Generated types Source: https://hub.awesomate.ai/docs/sdk/guides/generated-types/ Type every query, saved query and recipe from your own account's kinds, with one command. The SDK works without types, but with them your editor knows every kind, column, saved query and recipe in your account, and catches a misspelt column before you run anything. ```bash npx @awesomate/sdk types --out awesomate.d.ts ``` Then import the file once, anywhere in your project: ```ts import './awesomate'; const { rows } = await db.query('job', { where: { status: 'booked' } }); rows[0].status; // 'open' | 'quoted' | 'booked' | 'done' | null await db.call('book_job', { customer: contactId, title: 'Gutter clean' }); // arguments checked ``` ## Which account The command uses `AWESOMATE_TOKEN` when it is set. Otherwise it reads `~/.awesomate/credentials.json`, the file the Awesomate MCP keeps, choosing the profile from `--profile`, then `AWESOMATE_ACCOUNT`, then its default. It prints the account it used, so a wrong one is obvious. Pass `--base` to point at another hub. ## Keep them current Run the command again after you add a kind, change an attribute, or save a query or recipe. The file is generated from what the account's token may read, so attributes not marked readable by AI are left out, and the file says how many. --- # Your business details Source: https://hub.awesomate.ai/docs/sdk/guides/business-details/ Read what Awesomate knows about the business itself (name, what it does, voice, colours, how customers reach it), with where each detail came from. ```ts const biz = await db.business(); biz.name; // 'Brightwater Plumbing' for (const group of biz.groups) { for (const fact of group.facts) render(group.title, fact.label, fact.value, fact.status, fact.source); } biz.completeness; // { core_set: 7, core_total: 8, missing_core: ['logo'] } const waiting = await db.businessSuggestions(); // details waiting for the owner's yes ``` Each detail says where it came from. `confirmed` details are the ones agents and automations use. A `suggestion` waits for the owner, who confirms it in the hub under **Knowledge, Your business**. This needs the account's token (`hosting:read`), on every plan. An app server key cannot read it. To offer the details a business has not filled in yet, `businessCatalogue()` lists every detail a business can have, set or not, with its group and type (no values): ```ts const catalogue = await db.businessCatalogue(); const set = new Set(biz.groups.flatMap((g) => g.facts.map((f) => f.key))); const missing = catalogue.facts.filter((f) => !set.has(f.key)).map((f) => f.label); ``` ## Brand and voice documents Alongside the details, a business can have up to four long documents: its brand guide (`brand_guide`), its voice and style guide (`voice_guide`), the brand read from its website (`website_brand`) and a business summary (`business_summary`). ```ts const docs = await db.businessDocuments(); // which exist: kind, label, version, length, where from const voice = await db.businessDocument('voice_guide'); if (voice) render(voice.label, voice.body); // null when the business has no voice guide yet ``` The text is the owner's own. Use it for their business (an agent's instructions, site copy in their voice), and don't show it to anyone else. ## Your business map ```ts const map = await db.businessMap(); for (const department of map.departments) { // Envision, Form, Promise, Balance, Fulfil, Refine, Share, in that order for (const role of department.roles) render(department.verb, role.title, role.holders, role.helpers); } for (const step of map.path.steps) render(step.label, step.verb, step.owner.name, step.handoffRule); map.gaps[0]; // the most important thing missing, with a hub page to start it when there is one const roles = await db.businessMapRoles(); // every role: slug, title, department, holder const quotes = await db.businessMapRole('quotes'); quotes.helpers[0].instructionLine; // the line to put in that helper's instructions ``` The map is the business's structure: seven departments, each with three sub-departments, the roles in each, who holds them, and which agents and automations help which role, at what level (1 find out, through 5 report only the exceptions). Each person on the map has an `access` in the hub: owner, full or view. It is read only. The owner changes it on **Business map** in the hub, and your Claude Code can suggest changes for them to accept. `path` is how a customer moves through the business, step by step, with who looks after each step. When `path.stored` is false it is a suggestion for the business's kind of work. Each role's `proceduresTag` (such as `job:quotes`) is the tag its procedures carry in 1Brain; the map holds no procedure text. Each role also carries its `priorities`: the few things it must move each quarter (`quarter` such as `'2026-Q4'`, a title, a status, who owns it, an optional due date), and the map's `quarter` says which quarter is current. People on the account who hold no role yet are listed in `noRoleYet` on the Form department. > The business map is reaching accounts in stages. If it isn't on the account yet, a call answers with an `AwesomateError` whose `serverCode` is `feature_unavailable`. When `stored` is false, the owner has not started their map yet and what you see is worked out from what the account already runs. When `unavailable` is not empty, part of the account could not be read just now, so a department with no helper may simply not have been read. ### Moving from the first shape Before 0.24.0 the map used other names for the same things: a department was a `division`, a sub-department a `department`, and a role a `job`. So in that shape `departments` meant sub-departments. `businessMap()` now uses the words the owner sees, and `businessMapV1()` returns the old shape until v1 is removed (not before 2026-11-06). The CHANGELOG lists every renamed field. The same applies over HTTP: `/api/my-business/v2/map`, with `/roles` and `/roles/:slug` in place of `/jobs`. A suggested change uses the new names too: `role.add` with `departmentNo` (1 to 7) and `subDepartmentNo`, and `roleId` wherever v1 said `jobId`. --- # App client (browser) Source: https://hub.awesomate.ai/docs/sdk/reference/app-client/ createAppClient() and every method on it: signing in, reading and writing as the signed-in person, live lists, who is here, voice. The client for a web page or mobile app. People sign in by email link; every call then runs as them, inside the account's access rules. Generated from the SDK's own code. ## createAppClient() The browser client for one of the account's apps (Pro and above). Give it the app's publishable key, which is not a secret; never the account's token. ```ts createAppClient(options: AppClientOptions): AwesomateAppClient ``` | Parameter | Type | |---|---| | `options` | `AppClientOptions` | **Example** ```ts const app = createAppClient({ publishableKey: 'pk_...' }); app.auth.onChange((user) => render(user)); await app.auth.signInWithLink(email, { redirectTo: location.href }); ``` ## Signing in ### auth.signInWithLink() Email a sign-in link. The answer is the same whether or not the address can sign in. ```ts app.auth.signInWithLink( email: string, options: { redirectTo: string }, ): Promise<{ message: string; sent: boolean }> ``` | Parameter | Type | |---|---| | `email` | `string` | | `options` | `{ redirectTo: string }` | ### auth.completeSignIn() Finish signing in from the link: the current page's address by default, or a URL a deep link opened (Capacitor). Returns the user, or null when the address carries no sign-in. The token is removed from the browser's address bar. In a browser this happens by itself (see handleSignInLinks); calling it as well is safe, and returns the same sign-in, never a second. ```ts app.auth.completeSignIn(url?: string): Promise ``` | Parameter | Type | |---|---| | `url` (optional) | `string` | ### auth.onChange() Called with the user when someone signs in (or their details change: a new role, a linked contact) and with null on sign-out, including a refresh that was refused. A routine token refresh does not call it. ```ts app.auth.onChange(listener: (user: AppUser | null) => void): () => void ``` | Parameter | Type | |---|---| | `listener` | `(user: AppUser \| null) => void` | ### auth.onSignInError() Called when a sign-in link could not be used (expired, already used, not this app's). ```ts app.auth.onSignInError(listener: (err: AwesomateError) => void): () => void ``` | Parameter | Type | |---|---| | `listener` | `(err: AwesomateError) => void` | ### auth.user() The signed-in user as this client last saw them (no call to the hub), or null. ```ts app.auth.user(): Promise ``` ### auth.me() The signed-in person read fresh from the hub (their current role and linked contact), with the app's own settings: its name, its voice agent and whether it takes uploads. A person the account has disabled is signed out, and this rejects with code `unauthenticated`. ```ts app.auth.me(): Promise ``` ### auth.signOut() Sign out here and end the session at the hub. ```ts app.auth.signOut(): Promise ``` ### auth.accessToken() A current access token, for the app's own server to verify against the JWKS. ```ts app.auth.accessToken(): Promise ``` ## Reading ### query() Rows of one kind that this person may read, a page at a time. The database applies the kind's read rule for their role: a customer gets their own jobs, staff whatever theirs allows. A signed-in person, on an account on Pro and above. ```ts app.query( kind: K, options?: QueryOptions, S>, ): Promise, S>>, "hidden">> ``` | Parameter | Type | |---|---| | `kind` | `K` | | `options` (optional) | `QueryOptions, S>` | **Example** ```ts const { rows } = await app.query('job', { orderBy: ['created_at', 'desc'], limit: 20 }); ``` ### queryAll() Every matching row this person may read, a page at a time. maxRows (default 10,000) guards against reading a whole list by accident. Pro and above. ```ts app.queryAll( kind: K, options?: Omit, S>, "after"> & { maxRows?: number }, ): AsyncGenerator, S>> ``` | Parameter | Type | |---|---| | `kind` | `K` | | `options` (optional) | `Omit, S>, "after"> & { maxRows?: number }` | ### get() One record by id, or null when there is none this user may read. Ids are UUIDs: anything else names no record, so it answers null without asking the hub. Pro and above. ```ts app.get(kind: K, id: string): Promise | null> ``` | Parameter | Type | |---|---| | `kind` | `K` | | `id` | `string` | ## Writing ### write() Create a record (returns its id) or change one with options.id, inside the kind's write rule for this user. Pro and above. ```ts app.write( kind: K, data: Record | Partial>, options?: WriteOptions, ): Promise ``` | Parameter | Type | |---|---| | `kind` | `K` | | `data` | `Record \| Partial>` | | `options` (optional) | `WriteOptions` | ### archive() Archive a record, inside the kind's write rule for this person. Pro and above. ```ts app.archive(kind: K, id: string): Promise ``` | Parameter | Type | |---|---| | `kind` | `K` | | `id` | `string` | ### call() Run a recipe the account opened to this person's role: "accept this quote", something their write rule would not let them do by hand. Every step commits or none does; a recipe they may not run reads as not found. Never retried once sent: a write that may have landed is not safe to send twice (a refused sign-in is, and is retried after a refresh like every data call). Pro and above. ```ts app.call(recipe: R, args?: ArgsOf): Promise ``` | Parameter | Type | |---|---| | `recipe` | `R` | | `args` (optional) | `ArgsOf` | ## Live ### live() A list kept current: onRows gets the whole list (up to 200 rows, the query's order) at first and again whenever a row in it is added, changed, removed, or stops being one this person may read. One connection serves every live list on this client; it reconnects by itself and takes a fresh snapshot when it does. Returns stop(). Pro and above. ```ts app.live( kind: K, options: Omit, S>, "after">, handlers: LiveHandlers, S>> | ((rows: Pick, S>[], change: LiveChange, S>> | null) => void), ): () => void ``` | Parameter | Type | |---|---| | `kind` | `K` | | `options` | `Omit, S>, "after">` | | `handlers` | `LiveHandlers, S>> \| ((rows: Pick, S>[], change: LiveChange, S>> \| null) => void)` | ### here() Open a record (a job's conversation) for who-is-here: onPeople gets everyone else who has it open with their tab in front, and who is typing, at first and on every change; the app's assistant appears while it writes a reply. The hub checks this person may read the record, and keeps checking; onError hears a refusal. Shares the one live connection. Two parts of a page (a thread and a sidebar) can open the same record: each hears onPeople, and it stays open until both have left. Returns typing() and leave(). Pro and above. ```ts app.here( recordId: string, onPeople: (people: HerePerson[]) => void, onError?: (err: AwesomateError) => void, ): HereHandle ``` | Parameter | Type | |---|---| | `recordId` | `string` | | `onPeople` | `(people: HerePerson[]) => void` | | `onError` (optional) | `(err: AwesomateError) => void` | ## Staff ### lookupCustomers() Customers by name or email (two characters or more), or by id, so staff can find one and put a record under them. Only for a role the account lets look customers up in this app; anyone else gets a forbidden error. Name, email and phone only, at most 50. Pro and above. ```ts app.lookupCustomers(options: { ids?: string[]; limit?: number; q?: string }): Promise ``` | Parameter | Type | |---|---| | `options` | `{ ids?: string[]; limit?: number; q?: string }` | ## Ask by text ### ask() Ask the app's agent a typed question, as this signed-in person: a help box that answers from the business's content, with sources. The agent is the one the account picked for the app (the same one Talk uses). Show `questionNotice` with the answer when it comes (a person's first). The agent answers only from content this person may see; the voice persona does not apply. A refusal's serverCode says why: no_agent, channel_off, person_daily (30 a day per person), agent_daily, answers_used_up; personMessage is the sentence to show. ```ts app.ask(question: string, options?: { session?: string }): Promise ``` | Parameter | Type | |---|---| | `question` | `string` | | `options` (optional) | `{ session?: string }` | **Example** ```ts const a = await app.ask('What time do you open on Saturday?'); render(a.answer, a.sources, a.questionNotice); ``` ## Voice ### voiceAgent() The agent this app talks with by voice, or null when the account has not picked one. Pass it to AwesomateAgent.mount as agentId; the hub decides which agent answers, never the page. ```ts app.voiceAgent(): Promise ``` ### voiceSession() A voice session with the app's agent, as this signed-in person: `AwesomateAgent.mount({ agentId, session: () => app.voiceSession() })`. The agent greets them by name and, when the app's assistant is set up, knows the recent conversations they can see, and nothing more. A refusal's serverCode says why: no_voice_agent (the app has none), plan_required, consent_required, person_daily, agent_daily; its personMessage is the sentence to show the person, which the AwesomateAgent widget shows. ```ts app.voiceSession(): Promise ``` ## Files ### files.mode() Whether this app takes uploads: 'off', 'private' or 'public'. ```ts app.files.mode(): Promise<"public" | "private" | "off"> ``` ### files.upload() Upload one file, up to 10 MB. The name comes from a File, or pass one. A refusal's serverCode says why: uploads_off, too_large, type_not_allowed (public uploads take no web pages or scripts), files_storage_full; personMessage is the sentence to show. ```ts app.files.upload(file: Blob, options?: { name?: string }): Promise ``` | Parameter | Type | |---|---| | `file` | `Blob` | | `options` (optional) | `{ name?: string }` | ### files.download() The file a handle names, for someone signed in to this app. ```ts app.files.download(handle: string): Promise ``` | Parameter | Type | |---|---| | `handle` | `string` | --- # Server client Source: https://hub.awesomate.ai/docs/sdk/reference/server-client/ createClient() and every method on it: who the token is, queries, numbers, kinds and records, saved queries, write recipes, the business, bookings, files, tasks, app people, Knowledge and the support desk. The client for your server, a script or an n8n workflow, with the account's token or an app's server key. Both are secrets: never use them in a browser. Generated from the SDK's own code. ## createClient() The server client: the account's token (amt_pat_... with crm:read) or an app's server key (ak_...). Both are secrets: keep them on a server, never in a browser. ```ts createClient(options: ClientOptions): AwesomateClient ``` | Parameter | Type | |---|---| | `options` | `ClientOptions` | **Example** ```ts const db = createClient({ token: process.env.AWESOMATE_TOKEN! }); const { rows } = await db.query('contact', { limit: 10 }); ``` ## The token ### whoami() Who this token is: the account, its plan, the token's scopes and when it expires, and which features the account has (each with the sentence to say when it does not), and whose key it is: the account owner's, which acts as the owner, or a team member's own, which does only what that person can do in the hub. Needs hosting:read; an app key cannot ask (it reads and writes its own kinds only). ```ts db.whoami(): Promise ``` **Example** ```ts const me = await db.whoami(); if (!me.features?.bookings?.available) console.log(me.features?.bookings?.message); ``` ## Reading ### query() Rows of one kind that this token can read, a page at a time. The account's token (crm:read, every plan, Contacts on the account) or an app key, for the kinds it was made for. ```ts db.query(kind: K, options?: QueryOptions, S>): Promise, S>>> ``` | Parameter | Type | |---|---| | `kind` | `K` | | `options` (optional) | `QueryOptions, S>` | **Example** ```ts const { rows, next } = await db.query('contact', { where: { created_at: { gte: '$YEAR_BEGIN' } }, orderBy: ['created_at', 'desc'], limit: 50, }); ``` ### queryAll() Every matching row, a page at a time. maxRows guards against reading a whole list by accident. Needs what query() needs. ```ts db.queryAll( kind: K, options?: Omit, S>, "after"> & { maxRows?: number }, ): AsyncGenerator, S>> ``` | Parameter | Type | |---|---| | `kind` | `K` | | `options` (optional) | `Omit, S>, "after"> & { maxRows?: number }` | ### get() One row by id, or null when there is none this token can read. Ids are UUIDs: anything else names no row, so it answers null without asking the hub. Needs what query() needs. ```ts db.get(kind: K, id: string, options?: { tz?: string }): Promise | null> ``` | Parameter | Type | |---|---| | `kind` | `K` | | `id` | `string` | | `options` (optional) | `{ tz?: string }` | ### schema() The kinds this token can read: each kind's columns with their types, operators, choices and the owner's descriptions and sections, how many fields are hidden and why, the query grammar and examples. Works with the account's token (crm:read, every plan, Contacts on the account) and with an app key (its own kinds, and `access` says whether it can write). ```ts db.schema(): Promise ``` ### types() The readable kinds as a TypeScript file (what the types command writes). The account's token, crm:read. ```ts db.types(): Promise<{ as_at: string; content: string; file: string }> ``` ## Numbers ### metrics() What the contact list can be measured by: `people` (a count) and every number field the owner has named, each with its unit and other words for it. The catalogue for aggregate() and series(). The account's token, crm:read, every plan. ```ts db.metrics(): Promise ``` ### series() One metric over time, every bucket filled (a bucket nobody landed in is 0 for counts and sums): `range` is a preset (7d, 30d, 90d, 12m, mtd, qtd, ytd, fytd, this_month, last_month, this_fy, last_fy) or give `from` and `to` (YYYY-MM-DD); with neither, the last 12 months. `compare: 'previous'` adds the window before. At most 800 points. The account's token, crm:read, every plan. ```ts db.series( metric: string, options?: { agg?: "sum" | "avg" | "min" | "max"; anchor?: string; compare?: "previous"; from?: string; grain?: "day" | "week" | "month" | "quarter" | "year" | "fy"; range?: string; to?: string; tz?: string }, ): Promise ``` | Parameter | Type | |---|---| | `metric` | `string` | | `options` (optional) | `{ agg?: "sum" \| "avg" \| "min" \| "max"; anchor?: string; compare?: "previous"; from?: string; grain?: "day" \| "week" \| "month" \| "quarter" \| "year" \| "fy"; range?: string; to?: string; tz?: string }` | **Example** ```ts const s = await db.series('people', { range: '90d', grain: 'week', compare: 'previous' }); console.log(s.total, s.compare?.delta_pct); ``` ### datasets() The contact list as a dataset: its columns by kind (measures, dimensions to group by, and the dates people can be counted by). The account's token, crm:read. ```ts db.datasets(): Promise ``` ### dataset() One dataset by its id (`contact`), or null when there is none by that id. The account's token, crm:read. ```ts db.dataset(id: string): Promise ``` | Parameter | Type | |---|---| | `id` | `string` | ### aggregate() Grouped numbers (counts, sums) over the contact list, in the Business Data API's shape: the measures and dimensions datasets() lists. Counts and sums only, never a person. The account's token, crm:read, every plan; never an app key. ```ts db.aggregate( request: { dimensions?: string[]; filters?: { column: string; op?: string; value: unknown }[]; limit?: number; measures: { agg?: "count" | "sum" | "avg" | "min" | "max"; column: string }[]; order_by?: { dir?: "asc" | "desc"; field: string }[]; time?: { anchor?: string; from?: string; grain?: string; preset?: string; to?: string; tz?: string } }, ): Promise> ``` | Parameter | Type | |---|---| | `request` | `{ dimensions?: string[]; filters?: { column: string; op?: string; value: unknown }[]; limit?: number; measures: { agg?: "count" \| "sum" \| "avg" \| "min" \| "max"; column: string }[]; order_by?: { dir?: "asc" \| "desc"; field: string }[]; time?: { anchor?: string; from?: string; grain?: string; preset?: string; to?: string; tz?: string } }` | ## Kinds and records ### kinds() The app kinds this account has defined. The account's token, crm:read, every plan; never an app key. ```ts db.kinds(): Promise ``` ### defineKind() Define an app kind (a table an app keeps). No migration: registry rows and generated views. The account's token, crm:write, Support Plus and above; never an app key. ```ts db.defineKind(spec: KindSpec): Promise ``` | Parameter | Type | |---|---| | `spec` | `KindSpec` | ### addAttribute() Add an attribute to a kind you defined; records already there have no value for it. Answers the kind as it now is. The account's token, crm:write, Support Plus and above. ```ts db.addAttribute(kind: string, attribute: AttributeSpec): Promise ``` | Parameter | Type | |---|---| | `kind` | `string` | | `attribute` | `AttributeSpec` | ### archiveAttribute() Archive an attribute: it leaves every read and the generated types, and its values are kept. Answers the kind as it now is. The account's token, crm:write, Support Plus and above. ```ts db.archiveAttribute(kind: string, key: string): Promise ``` | Parameter | Type | |---|---| | `kind` | `string` | | `key` | `string` | ### write() Create a record (returns its id), or change one with options.id. Every value is checked against the kind. crm:write, Support Plus and above: the account's token, or an app key made with write access for this kind. ```ts db.write( kind: K, data: Record | Partial>, options?: WriteOptions, ): Promise ``` | Parameter | Type | |---|---| | `kind` | `K` | | `data` | `Record \| Partial>` | | `options` (optional) | `WriteOptions` | ### archive() Archive a record: it leaves every read and is kept for a restore. Needs what write() needs. ```ts db.archive(kind: K, id: string): Promise ``` | Parameter | Type | |---|---| | `kind` | `K` | | `id` | `string` | ## Saved queries ### queries() The saved queries on this account. crm:read, every plan: the account's token or an app key. ```ts db.queries(): Promise ``` ### saveQuery() Save a query by name (saving a name again replaces it). The spec is the query() grammar on one kind, with {"$param": ""} where a caller's value goes; it is compiled before it is stored, so a column it cannot read is refused now rather than when it runs. The account's token, crm:write, Support Plus and above; never an app key. ```ts db.saveQuery(spec: SavedQuerySpec): Promise ``` | Parameter | Type | |---|---| | `spec` | `SavedQuerySpec` | ### run() Run a saved query with its params. An optional param left out drops its condition. crm:read, every plan: the account's token or an app key. ```ts db.run( query: Q, params?: ParamsOf, options?: { after?: string; limit?: number; tz?: string }, ): Promise>> ``` | Parameter | Type | |---|---| | `query` | `Q` | | `params` (optional) | `ParamsOf` | | `options` (optional) | `{ after?: string; limit?: number; tz?: string }` | ### archiveQuery() Archive a saved query by its key. The account's token, crm:write, Support Plus and above. ```ts db.archiveQuery(key: string): Promise ``` | Parameter | Type | |---|---| | `key` | `string` | ## Write recipes ### recipes() The write recipes on this account. crm:read, every plan: the account's token or an app key. ```ts db.recipes(): Promise ``` ### saveRecipe() Save a write recipe: up to 10 write_record / archive_record steps run in one transaction. {"$param": ""} takes a caller's value, {"$step": ""} the id an earlier step wrote. The account's token, crm:write, Support Plus and above; never an app key. ```ts db.saveRecipe(spec: RecipeSpec): Promise ``` | Parameter | Type | |---|---| | `spec` | `RecipeSpec` | ### call() Run a write recipe. Every step commits or none does; a refusal names the step and the field. Never retried: a write that may have landed is not safe to send twice. crm:write, Support Plus and above: the account's token, or an app key with write access to every kind it writes. ```ts db.call(recipe: R, args?: ArgsOf): Promise ``` | Parameter | Type | |---|---| | `recipe` | `R` | | `args` (optional) | `ArgsOf` | ### archiveRecipe() Archive a write recipe by its key. The account's token, crm:write, Support Plus and above. ```ts db.archiveRecipe(key: string): Promise ``` | Parameter | Type | |---|---| | `key` | `string` | ## The business ### business() The business itself: the details Awesomate keeps about it (name, what it does, voice, colours, how customers reach it), grouped, each with where it came from. `confirmed` details are the ones agents and automations use; `suggestion` ones wait for the owner. Needs the account's token (hosting:read, every plan), never an app key. ```ts db.business(): Promise ``` ### businessCatalogue() Every detail a business can have (key, label, group, type and longest value), set or not, for a page that offers the missing ones. No values: business() has those. The account's token, hosting:read, every plan. ```ts db.businessCatalogue(): Promise ``` ### businessSuggestions() Details waiting for the owner's yes on Your business: from the website, the account, a team member or a file. The account's token, hosting:read, every plan. ```ts db.businessSuggestions(): Promise ``` ### businessDocuments() The business's long documents that exist (brand guide, voice guide, brand from the website, business summary): kind, version, length and where each came from, without the text. Needs the account's token (hosting:read, every plan), never an app key. ```ts db.businessDocuments(): Promise ``` ### businessDocument() One of the business's documents with its text (the current version), or null when the business has none of that kind yet. The text is the owner's own: use it for their business (agent instructions, site copy in their voice), never show it to anyone else. The account's token, hosting:read, every plan. ```ts db.businessDocument(kind: BusinessDocumentKind): Promise ``` | Parameter | Type | |---|---| | `kind` | `BusinessDocumentKind` | ### businessMap() The business map: the seven departments every business has, their sub-departments, the roles in each and who holds them, which agents and automations help which role and how far each may go, and what is missing, most important first. Read only: the owner changes the map in the hub. Needs the account's token (hosting:read, every plan), never an app key, and the account must have the business map. Since 0.24.0 in the words the owner sees; businessMapV1() is the old shape. ```ts db.businessMap(): Promise ``` ### businessMapRoles() Every role on the map, one line each: slug, title, department and who holds it. hosting:read, the business map on the account. ```ts db.businessMapRoles(): Promise ``` ### businessMapRole() One role by its slug, with the line each helper's instructions carry (`helpers[].instructionLine`): which role it helps, for whom, how far it may go and where its procedures are. hosting:read, the business map on the account. ```ts db.businessMapRole(slug: string): Promise ``` | Parameter | Type | |---|---| | `slug` | `string` | ### businessMapV1() The business map in its first shape, where a department is a `division`, a sub-department a `department` and a role a `job`. hosting:read, the business map on the account. ```ts db.businessMapV1(): Promise ``` ## Email series ### recordEvent() Tell Contacts something happened to someone ("job_paid", "quote_sent"), so any email series waiting for that event starts or stops for them. The person is found by email, or added; it never signs anyone up for email or changes their consent. Sending the same `key` twice records the event once (`duplicate: true`), so pass your own id for it. `record` names the quote, job or booking it is about. An event more than two days old is kept as a fact a series reads, never a start. Needs the account's token with crm:write (Support Plus and above), never an app key. The answer is the same whether or not the address belongs to someone erased from Contacts. ```ts db.recordEvent(event: SeriesEvent): Promise<{ duplicate: boolean }> ``` | Parameter | Type | |---|---| | `event` | `SeriesEvent` | **Example** ```ts await db.recordEvent({ event: 'job_paid', email: 'sam@example.com', record: 'job-1042', key: 'xero:INV-1042' }); ``` ## Bookings ### bookings.setup() How bookings are set up: whether they are on, the calendars (people, rooms) with their hours, the services with the calendars that take them, and this month's online bookings against the plan's. The `key`s here are what openTimes() and book() take. crm:read. ```ts db.bookings.setup(): Promise ``` ### bookings.list() Bookings that start in a window, soonest first, each with its customer. The window is from a day ago for 31 days unless told otherwise; `limit` is 1 to 500 (default 200). ```ts db.bookings.list( options?: { calendar?: string; from?: string | Date; limit?: number; status?: "cancelled" | "confirmed" | "completed" | "no_show"; to?: string | Date }, ): Promise ``` | Parameter | Type | |---|---| | `options` (optional) | `{ calendar?: string; from?: string \| Date; limit?: number; status?: "cancelled" \| "confirmed" \| "completed" \| "no_show"; to?: string \| Date }` | ### bookings.get() One booking by its id, or null when there is none (an id that is not a booking id never reaches the hub). ```ts db.bookings.get(id: string): Promise ``` | Parameter | Type | |---|---| | `id` | `string` | ### bookings.openTimes() The times a service can be booked, from now for two weeks unless told otherwise (at most 62 days at once). The same open times customers see; book() with `outsideHours` goes beyond them. ```ts db.bookings.openTimes( service: string, options?: { calendar?: string; from?: string | Date; seats?: number; to?: string | Date }, ): Promise ``` | Parameter | Type | |---|---| | `service` | `string` | | `options` (optional) | `{ calendar?: string; from?: string \| Date; seats?: number; to?: string \| Date }` | ### bookings.book() Book a time for a customer named by email (found in Contacts, or added; consent is never changed). Switches Bookings on if it was not. A time that is not open is refused with code `conflict` and `field` saying why: not_open, slot_taken, too_many_seats, session_full or day_full. Pass `idempotencyKey` so a retry returns the same booking. ```ts db.bookings.book( request: StaffBookingRequest, ): Promise<{ bookingId: string; created: boolean; endsAt: string; startsAt: string }> ``` | Parameter | Type | |---|---| | `request` | `StaffBookingRequest` | ### bookings.cancel() Cancel a booking. A booking already cancelled answers `cancelled: false`. ```ts db.bookings.cancel( id: string, options?: { notify?: boolean; reason?: string }, ): Promise<{ bookingId: string; cancelled: boolean; status: string | null }> ``` | Parameter | Type | |---|---| | `id` | `string` | | `options` (optional) | `{ notify?: boolean; reason?: string }` | ### bookings.move() Move a confirmed booking to a new start, on the same calendar or another (`calendar`). A time that is not open is refused with code `conflict` (field not_open, slot_taken, session_full or day_full) unless `outsideHours`; a booking that is not confirmed, or a time that has passed, with code `validation`. ```ts db.bookings.move( id: string, startsAt: string | Date, options?: { calendar?: string; notify?: boolean; outsideHours?: boolean }, ): Promise<{ bookingId: string; calendar: string; endsAt: string; startsAt: string }> ``` | Parameter | Type | |---|---| | `id` | `string` | | `startsAt` | `string \| Date` | | `options` (optional) | `{ calendar?: string; notify?: boolean; outsideHours?: boolean }` | ### bookings.outcome() Record how a booking went: the customer came (`completed`) or did not (`no_show`). Sends no email. ```ts db.bookings.outcome(id: string, status: "completed" | "no_show"): Promise ``` | Parameter | Type | |---|---| | `id` | `string` | | `status` | `"completed" \| "no_show"` | ## Files ### files.list() One folder; '' (the default) lists the top folders. ```ts db.files.list(path?: string): Promise ``` | Parameter | Type | |---|---| | `path` (optional) | `string` | ### files.search() Files by part of their name (* and ? are wildcards), across the folders or in one. At most 200: `truncated` says there were more. ```ts db.files.search( options?: { extensions?: string[]; q?: string; top?: "public" | "private" | "temp" }, ): Promise<{ results: FileEntry[]; truncated: boolean }> ``` | Parameter | Type | |---|---| | `options` (optional) | `{ extensions?: string[]; q?: string; top?: "public" \| "private" \| "temp" }` | ### files.usage() Space used against the plan's Files space. Measured, and up to ten minutes old. ```ts db.files.usage(): Promise ``` ### files.upload() Write a file (up to 50 MB). Refuses to replace one already there unless `overwrite`. A full Files space answers serverCode files_storage_full. ```ts db.files.upload(path: string, data: FileBody, options?: { overwrite?: boolean }): Promise ``` | Parameter | Type | |---|---| | `path` | `string` | | `data` | `FileBody` | | `options` (optional) | `{ overwrite?: boolean }` | ### files.download() A file's contents. ```ts db.files.download(path: string): Promise ``` | Parameter | Type | |---|---| | `path` | `string` | ### files.mkdir() Make a folder, and any above it. ```ts db.files.mkdir(path: string): Promise ``` | Parameter | Type | |---|---| | `path` | `string` | ### files.move() Move or rename. Anything using the old path or address needs the new one. ```ts db.files.move(from: string, to: string): Promise ``` | Parameter | Type | |---|---| | `from` | `string` | | `to` | `string` | ### files.makePublic() Move a file from private/ or temp/ to the same place under public/, and answer its public_url. Anyone with the address can open it. ```ts db.files.makePublic(path: string): Promise ``` | Parameter | Type | |---|---| | `path` | `string` | ### files.makePrivate() Move a file out of public/ into private/. Its public address stops serving it within seconds (a browser that already opened it may keep its own copy for a while). ```ts db.files.makePrivate(path: string): Promise ``` | Parameter | Type | |---|---| | `path` | `string` | ### files.remove() Move to the trash. Not erased, and no longer counted toward the Files space. ```ts db.files.remove(path: string): Promise<{ trashPath: string }> ``` | Parameter | Type | |---|---| | `path` | `string` | ### files.trash() What is in the trash, and how many bytes it holds. Needs files:read. ```ts db.files.trash(): Promise<{ entries: TrashEntry[]; total_bytes: number }> ``` ### files.emptyTrash() Erase what is in the trash, for good: the only call that deletes a file outright. With `olderThanDays` (1 to 3650), only what was removed longer ago than that. Needs files:write. An automation account that cannot empty its trash yet answers serverCode trash_empty_unavailable. ```ts db.files.emptyTrash( options?: { olderThanDays?: number }, ): Promise<{ bytes: number; files: number; removed: number }> ``` | Parameter | Type | |---|---| | `options` (optional) | `{ olderThanDays?: number }` | ### files.extract() Unpack a .zip in Files into a folder beside it, named after the archive (skills.zip unpacks to skills/). At most 4,000 entries and 250 MB unpacked. Needs files:write. ```ts db.files.extract(path: string): Promise<{ extractedTo: string; fileCount: number }> ``` | Parameter | Type | |---|---| | `path` | `string` | ### files.capabilities() Whether Files works for this account now, and why not: the plan, the automation account's version, and the owner's privacy switch, with the largest upload and the plan's space. Works with any account token, so check it before offering Files. ```ts db.files.capabilities(): Promise ``` ## Tasks ### tasks.board() The board: waiting now, put off, with someone else, recently decided. `all` shows everyone's, not only yours. hosting:read. ```ts db.tasks.board(options?: { scope?: "mine" | "all" }): Promise ``` | Parameter | Type | |---|---| | `options` (optional) | `{ scope?: "mine" \| "all" }` | ### tasks.give() Give a task. The person gets it on their board, and an email when they are not the one giving it. hosting:manage. ```ts db.tasks.give( task: GiveTask, ): Promise<{ emailed: boolean; holder: TaskPerson; id: number; key: string }> ``` | Parameter | Type | |---|---| | `task` | `GiveTask` | ### tasks.update() Change a given task: whoever wrote it, or the owner. hosting:manage. ```ts db.tasks.update(id: string | number, patch: Partial>): Promise ``` | Parameter | Type | |---|---| | `id` | `string \| number` | | `patch` | `Partial>` | ### tasks.done() Mark a given task done: the person who has it, whoever wrote it, or the owner. hosting:manage. ```ts db.tasks.done(id: string | number): Promise ``` | Parameter | Type | |---|---| | `id` | `string \| number` | ### tasks.drop() Take a given task off the board without doing it: whoever wrote it, or the owner. hosting:manage. ```ts db.tasks.drop(id: string | number): Promise ``` | Parameter | Type | |---|---| | `id` | `string \| number` | ### tasks.bringBack() Open a done or dropped task again, with the person who had it. They get an email unless they are the one bringing it back. Who may: see `decided[].canBringBack` on the board. hosting:manage. ```ts db.tasks.bringBack(id: string | number): Promise<{ emailed: boolean }> ``` | Parameter | Type | |---|---| | `id` | `string \| number` | ### tasks.record() What happened to a given task, in order: who gave it, who passed it on, who closed it and brought it back, and how long each person had it. The owner reads any task's record; anyone else only one they were part of. hosting:read. ```ts db.tasks.record(id: string | number): Promise ``` | Parameter | Type | |---|---| | `id` | `string \| number` | ### tasks.notNow() Put a card off until a time (at most 90 days), for this person only. hosting:manage. ```ts db.tasks.notNow(key: string, until: string | Date): Promise<{ until: string }> ``` | Parameter | Type | |---|---| | `key` | `string` | | `until` | `string \| Date` | ### tasks.cancelNotNow() Bring a card put off with notNow() back now, for this person. hosting:manage. ```ts db.tasks.cancelNotNow(key: string): Promise ``` | Parameter | Type | |---|---| | `key` | `string` | ### tasks.passOn() Hand a card to someone on the account who can act on it. hosting:manage. ```ts db.tasks.passOn( key: string, toEmail: string, reason?: string, ): Promise<{ emailed: boolean; holder: TaskPerson }> ``` | Parameter | Type | |---|---| | `key` | `string` | | `toEmail` | `string` | | `reason` (optional) | `string` | ### tasks.decisions() The decisions on the Tasks page: waiting now, put off, said yes to and gone further up, and decided in the last 14 days. A token is no one person, so `mine` shows only decisions waiting on the owners; pass `scope: 'all'` for the rest, including what was decided. Read only: deciding is a person's tap in the hub, never a token's, so every card's `actions` is empty here. hosting:read; refused with serverCode `not_live` where decisions are not switched on yet. ```ts db.tasks.decisions(options?: { scope?: "mine" | "all" }): Promise ``` | Parameter | Type | |---|---| | `options` (optional) | `{ scope?: "mine" \| "all" }` | ### tasks.decisionRecord() What happened to a decision, in order, with how long each person had it. A decision this token may not see reads as not found (serverCode `gone`). hosting:read. ```ts db.tasks.decisionRecord(taskId: string | number): Promise ``` | Parameter | Type | |---|---| | `taskId` | `string \| number` | ## App people ### appUsers.list() Everyone who can sign in, newest first (at most 1,000), with their role, last sign-in and whether they are disabled. ```ts db.appUsers.list(): Promise ``` ### appUsers.invite() Let someone sign in, with a role (default `member`); a person already on the list keeps their id and takes the new role. No email is sent: they sign in from your app with auth.signInWithLink(). They are linked to the contact in Contacts with the same email. ```ts db.appUsers.invite(email: string, options?: { role?: string }): Promise ``` | Parameter | Type | |---|---| | `email` | `string` | | `options` (optional) | `{ role?: string }` | ### appUsers.update() Change someone's role, disable or enable them, or link them to a contact (`contactId`, or null to unlink). Disabling ends every session they have at once; `sessionsRevoked` says how many. A role is lower case: letters, digits and _. ```ts db.appUsers.update( id: string, patch: { contactId?: string | null; disabled?: boolean; role?: string }, ): Promise<{ sessionsRevoked: number; user: AppPerson }> ``` | Parameter | Type | |---|---| | `id` | `string` | | `patch` | `{ contactId?: string \| null; disabled?: boolean; role?: string }` | ### assistantUsage() One app's AI assistant this month (UTC): how many conversations it looked at, replied to or drafted for, how many replies asked Knowledge (those count towards Knowledge answers), and the AI tokens, with how many were on the account's own AI key. The account's token, crm:read. ```ts db.assistantUsage(appId: string): Promise ``` | Parameter | Type | |---|---| | `appId` | `string` | ## Knowledge ### knowledge.ask() A cited answer from the account's own content. `grounded` is true only when the answer came with sources: an answer with none came from the model, not the content, so never present it as the business's. `session` keeps a conversation going. ```ts db.knowledge.ask( question: string, options?: { filters?: KnowledgeFilters; session?: string }, ): Promise ``` | Parameter | Type | |---|---| | `question` | `string` | | `options` (optional) | `{ filters?: KnowledgeFilters; session?: string }` | ### knowledge.search() Instant search over the library: hits with highlighted snippets and facet counts for the current filters. A facet given several values matches any of them. Snippets mark the matched words between `highlight.pre` and `highlight.post`. `limit` is 1 to 50. ```ts db.knowledge.search(options?: KnowledgeSearchOptions): Promise ``` | Parameter | Type | |---|---| | `options` (optional) | `KnowledgeSearchOptions` | ### knowledge.data() Grouped numbers from Your numbers: a dataset by its id (the Data tab lists them; built-in ones are `ds-builtin-`, such as `ds-builtin-google-analytics-daily`), one to eight measures, and optional dimensions, filters and time. The answer is the platform's own shape: columns and rows, plus what it assumed. ```ts db.knowledge.data(query: KnowledgeDataQuery): Promise> ``` | Parameter | Type | |---|---| | `query` | `KnowledgeDataQuery` | ## Support desk ### supportDesk.status() Whether the desk is on, the address mail is forwarded to, how its AI answers, and how many tickets are in each status. ```ts db.supportDesk.status(): Promise ``` ### supportDesk.tickets() Tickets in one status (default `open`, or `all`), most recent first; `limit` 1 to 100 (default 25). ```ts db.supportDesk.tickets( options?: { limit?: number; status?: "all" | SupportTicketStatus }, ): Promise<{ counts: Record; tickets: SupportTicketSummary[] }> ``` | Parameter | Type | |---|---| | `options` (optional) | `{ limit?: number; status?: "all" \| SupportTicketStatus }` | ### supportDesk.ticket() One ticket with its messages, the team's notes and any reply waiting for a person's OK, or null when there is none (an id that is not a ticket id never reaches the hub). ```ts db.supportDesk.ticket(id: string): Promise ``` | Parameter | Type | |---|---| | `id` | `string` | --- # Bookings client Source: https://hub.awesomate.ai/docs/sdk/reference/bookings-client/ createBookingsClient() and every method on it: what can be booked and when, booking, and the customer's own link. The client for a business's own booking page, for visitors who are not signed in. It uses the booking key from Contacts, Bookings, which is public and works only on the sites it lists. Generated from the SDK's own code. ## createBookingsClient() The client for a business's own booking page. Use the booking key from Contacts, Bookings, On your website; it works on every plan, from the sites the key lists. ```ts createBookingsClient(options: BookingsClientOptions): AwesomateBookingsClient ``` | Parameter | Type | |---|---| | `options` | `BookingsClientOptions` | **Example** ```ts import { createBookingsClient } from '@awesomate/sdk'; const bookings = createBookingsClient({ key: 'bk_your_booking_key' }); const { services } = await bookings.services(); const times = await bookings.openTimes(services[0].key); ``` ## What can be booked ### services() The business's name and what it offers to book. ```ts bookings.services(): Promise<{ business: string; services: BookableService[] }> ``` ### openTimes() The times a service can be booked, soonest first, from now for two weeks unless told otherwise (at most 62 days at once). Times are UTC; show them in the calendar's own zone. ```ts bookings.openTimes( service: string, options?: { calendar?: string; from?: string | Date; seats?: number; to?: string | Date }, ): Promise ``` | Parameter | Type | |---|---| | `service` | `string` | | `options` (optional) | `{ calendar?: string; from?: string \| Date; seats?: number; to?: string \| Date }` | ## Booking ### book() Book a time for a visitor. They get an email with an invite and a link to change or cancel; the business gets a notice. A refusal has code `conflict` and `field` saying why: - `not_open`, `slot_taken`, `too_many_seats`, `session_full`, `day_full`: the time is not free (taken since you listed it, or not enough places left): list the times again; - `too_many_open`: this email already has three bookings coming up; - `monthly_limit`: the business has taken its plan's online bookings for the month. Show `personMessage` to the visitor for the last two. ```ts bookings.book(request: BookingRequest): Promise ``` | Parameter | Type | |---|---| | `request` | `BookingRequest` | ## The customer's link ### booking() One booking, from the token in its manage link (manageTokenFrom()), with how the business looks so the page can match it. ```ts bookings.booking( manageToken: string, ): Promise<{ booking: ManagedBooking; business: string; look: BookingPageLook }> ``` | Parameter | Type | |---|---| | `manageToken` | `string` | ### openTimesToMove() The times a booking could move to, leaving its own time out. ```ts bookings.openTimesToMove( manageToken: string, options?: { from?: string | Date; to?: string | Date }, ): Promise ``` | Parameter | Type | |---|---| | `manageToken` | `string` | | `options` (optional) | `{ from?: string \| Date; to?: string \| Date }` | ### move() Move a booking to a time from openTimesToMove(). Refused with code `conflict` and field `too_late` once changes have closed, or `not_open` (or another of book()'s reasons) when the time is no longer free. ```ts bookings.move(manageToken: string, startsAt: string): Promise<{ endsAt: string; startsAt: string }> ``` | Parameter | Type | |---|---| | `manageToken` | `string` | | `startsAt` | `string` | ### cancel() Cancel a booking from its manage link. A second cancel answers cancelled: false. Once changes have closed (`canCancel` false) it is refused with code `conflict`, field `too_late`. ```ts bookings.cancel(manageToken: string, reason?: string): Promise<{ cancelled: boolean }> ``` | Parameter | Type | |---|---| | `manageToken` | `string` | | `reason` (optional) | `string` | --- # Errors Source: https://hub.awesomate.ai/docs/sdk/reference/errors/ AwesomateError, what each error code means, and which sentence to show the person. Every failure is an `AwesomateError`. Branch on `code`, show `personMessage` to the person when it is there, and log `message` and `serverCode` for yourself. ```ts import { AwesomateError } from '@awesomate/sdk'; try { await app.call('accept_quote', { job: job.id }); } catch (err) { if (err instanceof AwesomateError && err.code === 'not_found') showMessage('That quote is no longer open.'); else throw err; } ``` ## Codes | `code` | What it means | |---|---| | `unauthenticated` | No valid sign-in or token. In an app, sign the person in again; on a server, check the token. | | `forbidden` | Signed in, but not allowed: the role, or a key that does not cover this kind. | | `not_found` | Nothing there that this caller may see. A record they may not read looks exactly like one that does not exist. | | `validation` | A value the kind does not accept, or a query that names a column it cannot read. `field` names it. | | `consent_blocked` | The account has switched off what this call needs, in its Privacy or Features settings. Only the owner can switch it on, in the hub. | | `rate_limited` | Too many calls in a short time. Wait a little and try again. | | `conflict` | Something changed while the call ran (a kind's fields were edited). A read is retried once for you. | | `unavailable` | Anything else, including the hub being briefly unreachable. `serverCode` has the hub's own detail: `payment_suspended` means the account is on hold for an unpaid bill, and its apps and keys stop until the owner pays in the hub (do not retry in a loop). | | `feature_unavailable` | The account does not have this feature. `reason` says why (not_released, turned_off_by_awesomate, not_on_plan, switched_off_by_you), `feature` names it, and `upgrade` is set when a plan includes it. Say `message` and stop; never retry. | | `upgrade_required` | This needs a higher plan. `requiredPlan` names it, and `upgrade` gives the hub page when the hub sent one. | | `scope_missing` | The token lacks a scope this call needs. `missingScopes` names them: make a token with them in the hub. | ## AwesomateError Every failure from the hub. `message` is for you, the developer; `personMessage`, when the hub sent one, is the sentence to show the person using your app. A refusal over a feature, a plan or a token's scopes says what it is about in `reason`, `feature`, `missingScopes`, `upgrade` and `requiredPlan`. | Property | Type | | |---|---|---| | `message` | `string` | For you, the developer. | | `cause` (optional) | `unknown` | | | `code` | `"unauthenticated" \| "forbidden" \| "not_found" \| "validation" \| "consent_blocked" \| "rate_limited" \| "conflict" \| "unavailable" \| "feature_unavailable" \| "upgrade_required" \| "scope_missing"` | Why it failed. | | `feature` (optional) | `string` | With feature_unavailable: the feature's key. | | `field` (optional) | `string` | The column or parameter a validation error is about. | | `message` | `string` | | | `missingScopes` (optional) | `string[]` | With scope_missing: the scopes the token lacks. Make a token with them in the hub. | | `name` | `string` | | | `personMessage` (optional) | `string` | A sentence for the person using the app, when the hub sent one (a refused voice call: "You've used today's voice time"). | | `reason` (optional) | `string` | With feature_unavailable: why the account does not have the feature (not_released, turned_off_by_awesomate, not_on_plan, switched_off_by_you). | | `requiredPlan` (optional) | `string` | The plan this needs, when the hub named one. | | `serverCode` (optional) | `string` | The hub's own code, unmapped. | | `stack` (optional) | `string` | | | `status` | `number` | The HTTP status, or 0 when the SDK refused before sending. | | `upgrade` (optional) | `{ plan: string; url: string }` | The plan that includes it and the hub page to upgrade on, when the hub said. | | `stackTraceLimit` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | ## ErrorCode Why a call failed, one of a fixed list: unauthenticated, forbidden, not_found, validation, consent_blocked, rate_limited, conflict, unavailable, feature_unavailable, upgrade_required, scope_missing. The hub's own detail is in serverCode. ```ts type ErrorCode = typeof ERROR_CODES[number] ``` --- # Types Source: https://hub.awesomate.ai/docs/sdk/reference/types/ Every type @awesomate/sdk exports: query options, pages, app users, live updates, kinds, saved queries and recipes. Every type the SDK exports, generated from its code. Rows are typed from your own account once you run `npx @awesomate/sdk types` (see [Generated types](../guides/generated-types.md)). ## Queries ### QueryOptions How a query picks, orders, pages and narrows rows. `interface QueryOptions` | Property | Type | | |---|---|---| | `after` (optional) | `string` | The previous page's next. Repeat the same orderBy. | | `limit` (optional) | `number` | Rows per page, 1 to 1,000. Default 50. | | `orderBy` (optional) | `[Extract<{ [C in string \| number \| symbol]: NonNullable extends unknown[] ? never : C }[keyof T], string>, "asc" \| "desc"]` | One sort column; id breaks ties. Default: created_at, newest first. | | `select` (optional) | `S[]` | Only these columns. Default: every column this caller may read. | | `tz` (optional) | `string` | IANA zone for dates and time variables. Default Australia/Sydney. | | `where` (optional) | `Where` | Which rows: a filter per column. | ### Where A where clause: a filter per column, combined with and, or and not. ```ts type Where = { [C in keyof T]?: ColumnFilter> } & { and?: Where[]; not?: Where; or?: Where[] } ``` ### ColumnFilter What one column accepts in a where clause: a bare value means equals (a list means in, or has all for a list column). The checks are wrapped in [ ] so a choice column ('open' | 'booked') is taken whole: `{ in: ['open', 'booked'] }` is one filter, not one per choice. ```ts type ColumnFilter = [V] extends [(infer E)[]] ? E | E[] | ListOps : [V] extends [number] ? V | V[] | null | NumberOps : [V] extends [boolean] ? V | null | BoolOps : [V] extends [string] ? V | V[] | null | TextOps : unknown ``` ### TextOps Filters on a text column. gt/gte/lt/lte also take time variables such as `$YEAR_BEGIN` on dates. ```ts type TextOps = undefined ``` ### NumberOps Filters on a number column. ```ts type NumberOps = undefined ``` ### BoolOps Filters on a yes/no column. ```ts type BoolOps = undefined ``` ### ListOps Filters on a list column (tags, choices): has one, any or all, or is empty. ```ts type ListOps = undefined ``` ### SortableColumn Columns that can be sorted by: anything but a list. ```ts type SortableColumn = Extract<{ [C in keyof T]: NonNullable extends unknown[] ? never : C }[keyof T], string> ``` ### Page One page of rows. Pass `next` as `after` for the following page. `interface Page` | Property | Type | | |---|---|---| | `applied` | `Applied` | What the hub assumed; repeat it to whoever asked. | | `asAt` | `string` | When the hub read the rows. | | `count` | `number` | Rows on this page. | | `hidden` | `{ not_readable_by_ai: number; sensitive: number }` | Columns left out: not marked readable by AI (to the account's token), or sensitive. | | `next` | `string \| null` | Where the next page starts, or null on the last page. | | `rows` | `R[]` | The rows on this page. | ### Applied What the hub assumed for a query (order, limit, time zone, what each time variable meant), so it can be said back. `interface Applied` | Property | Type | | |---|---|---| | `defaults` | `string[]` | | | `limit` | `number` | | | `order_by` | `[string, "asc" \| "desc"]` | | | `time_variables` | `Record` | | | `timezone` | `string` | | ## Generated types ### Kinds Augmented by the generated awesomate.d.ts, so each kind's rows are typed. ```ts interface Kinds {} ``` ### KindName A kind's name: one of yours once the generated types are imported, any string before then. ```ts type KindName = [keyof Kinds] extends [never] ? string : Extract ``` ### RowOf A row of kind K: typed from the generated file, or a plain record before then. ```ts type RowOf = K extends keyof Kinds ? Kinds[K] : Record ``` ### Queries Augmented by the generated awesomate.d.ts: each saved query's params and row. ```ts interface Queries {} ``` ### QueryName A saved query's key: one of yours once the generated types are imported, any string before then. ```ts type QueryName = [keyof Queries] extends [never] ? string : Extract ``` ### ParamsOf The parameters saved query Q takes. ```ts type ParamsOf = Q extends keyof Queries ? Queries[Q] extends { params: infer P } ? P : never : Record ``` ### QueryRowOf The row saved query Q returns. ```ts type QueryRowOf = Q extends keyof Queries ? Queries[Q] extends { row: infer R } ? R : never : Record ``` ### Recipes Augmented by the generated awesomate.d.ts: each write recipe's args. ```ts interface Recipes {} ``` ### RecipeName A write recipe's key: one of yours once the generated types are imported, any string before then. ```ts type RecipeName = [keyof Recipes] extends [never] ? string : Extract ``` ### ArgsOf The arguments recipe R takes. ```ts type ArgsOf = R extends keyof Recipes ? Recipes[R] extends { args: infer A } ? A : never : Record ``` ## App client ### AppClientOptions Options for createAppClient(). `interface AppClientOptions` | Property | Type | | |---|---|---| | `baseUrl` (optional) | `string` | Default https://hub.awesomate.ai. | | `fetch` (optional) | `(input: URL \| RequestInfo, init: RequestInit) => Promise` | Your own fetch. Default: the global one. | | `handleSignInLinks` (optional) | `boolean` | Default true in a browser: a sign-in link is taken up by itself, when the page loads with one and when one arrives in a tab already showing the page (only the #fragment changes then, and the page does not reload). Set false when another client on the page handles links. | | `publishableKey` | `string` | The app's publishable key (pk_...). Not a secret: it belongs in browser code. | | `storage` (optional) | `SessionStorageLike` | Default: localStorage in a browser, memory elsewhere. In Capacitor, pass Preferences. | | `storageKey` (optional) | `string` | Default: one per publishable key. | | `WebSocket` (optional) | `WebSocketLike` | For live(): the WebSocket class. Default: the browser's (and Node 22's) global WebSocket. | ### AppUser The signed-in person. `interface AppUser` | Property | Type | | |---|---|---| | `contact_id` | `string \| null` | Their record in the account's Contacts, when linked. | | `email` | `string` | The address they sign in with. | | `id` | `string` | Their app user id. | | `role` | `string` | owner, staff, member or one of the account's own; the database re-reads it on every call | ### Customer A customer as lookupCustomers() returns them. A field the owner marked sensitive is null. `interface Customer` | Property | Type | | |---|---|---| | `email` | `string \| null` | | | `id` | `string` | | | `name` | `string \| null` | | | `phone` | `string \| null` | | ### LiveHandlers What live() calls. `interface LiveHandlers` | Property | Type | | |---|---|---| | `onError` (optional) | `(err: AwesomateError) => void` | A refusal for this list (a column that does not exist, too many lists); the list stops. | | `onRows` | `(rows: R[], change: LiveChange \| null) => void` | Called with the whole list, in the query's order, at first and after every change. | ### LiveChange What changed since onRows was last called. `interface LiveChange` | Property | Type | | |---|---|---| | `removes` | `string[]` | Ids that left the list. | | `upserts` | `R[]` | Rows that are new or changed since the last call. | ### HerePerson Someone else with the same record open (app.here), as the hub sends them. `interface HerePerson` | Property | Type | | |---|---|---| | `assistant` (optional) | `true` | The app's AI assistant, writing a reply. | | `name` | `string \| null` | Their first name from the account's Contacts, or null. | | `role` | `string` | | | `typing` | `boolean` | | | `user` | `string` | Their app user id, or 'assistant' for the app's AI assistant. | ### HereHandle From here(): say this person is typing, or close the record. `interface HereHandle` | Property | Type | | |---|---|---| | `leave` | `() => void` | Close the record: the others stop seeing this person on it once every part of the page that opened it has left. | | `typing` | `(on: boolean) => void` | Say this person is typing (call it on each keystroke: it sends at most every few seconds) or has stopped (on send or blur). Without another call, typing ends on its own after a few seconds. | ### VoiceSession What AwesomateAgent.mount's session() returns: pass it through unchanged. `interface VoiceSession` | Property | Type | | |---|---|---| | `agent_name` | `string` | | | `expires_at` | `string` | | | `max_seconds` | `number` | | | `pages` | `{ label: string; path: string }[]` | | | `say_as` | `Record` | | | `session_id` | `string` | | | `token` | `string` | | | `url` | `string` | | ### SessionStorageLike Where a session is kept: localStorage, Capacitor Preferences, or anything with these three. `interface SessionStorageLike` | Property | Type | | |---|---|---| | `getItem` | `(key: string) => string \| Promise \| null` | The stored value, or null. | | `removeItem` | `(key: string) => void \| Promise` | Forget a value. | | `setItem` | `(key: string, value: string) => void \| Promise` | Store a value. | ### WebSocketLike The part of the WebSocket API live() uses: the browser's, Node 22's, or the ws package's. `interface WebSocketLike` | Property | Type | | |---|---|---| | `constructor` | `(url: string) => { onclose: ((ev: { code: number; reason: string }) => void) \| null; onerror: ((ev: unknown) => void) \| null; onmessage: ((ev: { data: unknown }) => void) \| null; onopen: ((ev: unknown) => void) \| null; readyState: number; close: any; send: any }` | | ### signInTokenFrom The sign-in token a link carries, from a URL's fragment (#awesomate_token=...), or null. ```ts signInTokenFrom(url: string): string | null ``` ### AppMe The signed-in person and their app, from auth.me(). `interface AppMe` | Property | Type | | |---|---|---| | `app` | `{ file_uploads: "public" \| "private" \| "off"; id: string; name: string; voice_agent_id: string \| null }` | | | `user` | `AppUser` | | ### AppTokenClaims The claims of a verified app access token, from verifyAppToken(). `interface AppTokenClaims` | Property | Type | | |---|---|---| | `app` | `string` | The app's id. | | `contact` | `string \| null` | Their contact in Contacts, when linked. | | `exp` | `number` | When the token expires, in seconds since 1970. | | `iat` | `number \| null` | When it was made, in seconds since 1970. | | `role` | `string` | Their role when the token was made: a hint only, as the hub re-reads it on every call. | | `sub` | `string` | The person's app user id. | ### verifyAppToken Verify an access token one of the account's apps sent your own server (from app.auth.accessToken()), against the hub's public keys: ES256, signed by the hub, for this app, not expired. Answers its claims; anything else rejects with code `unauthenticated`. The keys are fetched from `/api/sdk/v1/jwks.json` and kept for five minutes. Treat `role` as a hint: the hub reads the person's current role on every call it answers. ```ts verifyAppToken( token: string, options: { appId: string; baseUrl?: string; fetch?: { (input: URL | RequestInfo, init?: RequestInit): Promise; (input: string | URL | Request, init?: RequestInit): Promise }; issuer?: string }, ): Promise ``` **Example** ```ts import { verifyAppToken } from '@awesomate/sdk'; const accessToken = 'the token your page sent, from app.auth.accessToken()'; const claims = await verifyAppToken(accessToken, { appId: 'app_your_app_id' }); console.log(claims.sub, claims.role); ``` ## Agent ### AgentAnswer An answer from the app's agent to a typed question (app.ask()). `interface AgentAnswer` | Property | Type | | |---|---|---| | `answer` | `string` | | | `questionNotice` | `string \| null` | The platform's sentence about keeping questions it could not answer. It comes with a person's first answer only: show it with that answer. | | `sources` | `{ title: string; url: string \| null }[]` | Where the answer came from: show them with it. | | `status` | `"answered" \| "none"` | answered: from the business's content, with sources; none: the content has no answer (or not for this person). | ## Errors ### AwesomateErrorDetails What the hub said about a refusal beyond its code: the typed fields of an AwesomateError. `interface AwesomateErrorDetails` | Property | Type | | |---|---|---| | `feature` (optional) | `string` | With feature_unavailable: the feature's key (contacts, bookings, knowledge, tasks...). | | `missingScopes` (optional) | `string[]` | With scope_missing: the scopes the token lacks for this call. | | `reason` (optional) | `string` | With feature_unavailable: not_released, turned_off_by_awesomate, not_on_plan or switched_off_by_you. | | `requiredPlan` (optional) | `string` | The plan this needs, when the hub named one. | | `upgrade` (optional) | `{ plan: string; url: string }` | The plan that includes what was asked for, and the hub page to upgrade on, when the hub said. | ## Files ### FileEntry A file or folder in the account's Files (the folders on its automation account: public/, private/, temp/). `interface FileEntry` | Property | Type | | |---|---|---| | `modified` | `string` | ISO time it last changed. | | `name` | `string` | | | `path` | `string` | Relative to the files folder, starting with its top folder: public/images/logo.png. | | `public_url` | `string \| null` | For a file in public/: the address anyone with it can open, with no login. Otherwise null. | | `size` | `number` | Bytes; 0 for a folder. | | `type` | `"file" \| "dir"` | | ### FilesUsage How much of the plan's Files space is used. `interface FilesUsage` | Property | Type | | |---|---|---| | `files` | `number` | | | `limit_bytes` | `number` | | | `measured_at` | `string` | | | `partial` | `boolean` | The measure stopped early on a very large tree: used_bytes is at least this much. | | `used_bytes` | `number` | | ### TrashEntry Something in the Files trash, from files.trash(). `interface TrashEntry` | Property | Type | | |---|---|---| | `bytes` | `number` | | | `deleted_at` | `string \| null` | When it was removed, or null when the trash name does not say. | | `name` | `string` | What it was called before it was removed. | | `path` | `string` | Where it sits in the trash. | | `type` | `"file" \| "dir"` | | ### AppFile A file uploaded from an app. Keep `handle` in a record (a kind's file attribute); it opens the file again. `interface AppFile` | Property | Type | | |---|---|---| | `handle` | `string` | | | `name` | `string` | | | `public_url` | `string \| null` | Only when the app's uploads are public. | | `size` | `number` | | ### FileBody What a file can be sent as. ```ts type FileBody = Blob | ArrayBuffer | Uint8Array | string ``` ### FilesCapabilities What Files can do for this account now, from files.capabilities(). `interface FilesCapabilities` | Property | Type | | |---|---|---| | `consentGranted` | `boolean` | The owner has switched on "Open your automation account's file folders" in Settings, Privacy. | | `domainName` | `string \| null` | | | `enabled` | `boolean` | All three: Files works now. | | `maxUploadBytes` | `number` | | | `plan` | `string \| null` | | | `planEligible` | `boolean` | The plan includes Files (Support Plus, Pro, Embedded). | | `requiredPlans` | `string[]` | | | `storageLimitBytes` | `number` | | | `variantEligible` | `boolean` | The automation account runs a version that has Files. | | `variantName` | `string \| null` | | ## Tasks ### TaskBoard The Tasks board, as the account's token sees it (the owner, so `scope: 'all'` works). `interface TaskBoard` | Property | Type | | |---|---|---| | `canChooseScope` | `boolean` | Whether this person may ask for `scope: 'all'`: the owner only. | | `canGive` | `boolean` | Whether this person may give tasks (everyone but view-only people). | | `decided` | `{ at: string; by: TaskPerson \| null; canBringBack: boolean; from: string; key: string; kind: string; text: string }[]` | Recently decided, for the record. `canBringBack` is true for a given task this person may open again with bringBack(): the number after `task:` in its key is the task's id. | | `hasTeam` | `boolean` | Whether anyone besides the owner is on the account. | | `later` | `TaskCard[]` | Put off with Not now; back when the time comes. | | `me` | `TaskPerson & { role: string }` | | | `people` | `(TaskPerson & { me: boolean })[]` | Who a task can be given to. | | `scope` | `"mine" \| "all"` | | | `toDecide` | `TaskCard[]` | Waiting for a decision now. | | `unavailable` | `string[]` | Places the board could not read just now, in the owner's words. When it is not empty the board may be missing cards from them: say so rather than treating the board as complete. | | `waiting` | `TaskCard[]` | With someone else. | ### TaskCard One card on the account's Tasks board: something waiting on a person (a quote to accept, an email to approve) or a task someone gave. The card's main action happens on its own page (`openPath` in the hub); Tasks only moves the card (not now, pass on) and finishes given tasks. `interface TaskCard` | Property | Type | | |---|---|---| | `action` | `TaskCardAction \| null` | The card's one-tap action, by name, for someone who may act on it; null otherwise. The hub page maps each name onto its own call, so a card never names an address. | | `canAct` | `boolean` | | | `context` | `string \| null` | | | `createdAt` | `string` | | | `dueAt` | `string \| null` | | | `from` | `string` | Where it came from, in the owner's words ("Build requests", "Contacts"). | | `holder` | `TaskPerson \| null` | Who has it, when it was passed on; null means anyone on the account. | | `key` | `string` | Stable id of the card; pass it to notNow() and passOn(). | | `kind` | `string` | | | `openLabel` | `string` | The words on the button that opens it ("Review the quote"). | | `openPath` | `string` | The hub page that shows it in full. | | `ownerOnly` | `boolean` | Only the owner may act on it (nobody else on the account can, even when it is passed on). | | `passedBy` | `{ agent: boolean; at: string; email: string; given: boolean; name: string \| null; reason: string \| null } \| null` | Who passed it on, and when. `given` is true for a task given to this person (it reads "gave you this task"), `agent` when an agent asked for it. Null when it was never passed on. | | `passTo` | `TaskPerson[]` | Who it can be passed to. | | `priority` | `number` | 1 routine to 5 urgent. | | `question` | `string` | What is being asked, as a question. | | `snoozedUntil` | `string \| null` | When it comes back, if put off with Not now. | | `task` | `{ author: TaskPerson; canDrop: boolean; canEdit: boolean; canFinish: boolean; detail: string \| null; id: number; linkPath: string \| null; raisedBy: { agentId: string; agentName: string } \| null } \| null` | Set for a task someone gave (give()); null for everything else. | | `title` | `string` | | | `type` | `"yes_no" \| "approve_draft" \| "pick_one" \| "pick_many" \| "input" \| "inform"` | What kind of answer it asks for: yes_no, approve_draft, pick_one, pick_many, input, or inform ("just so you know"). | ### TaskCardAction A card's one-tap action, by name (the hub page behind the card runs it). ```ts type TaskCardAction = { name: "accept_quote"; requestId: string } | { name: "answer_request_question"; question: string; requestId: string } | { name: "answer_business_suggestion"; proposalId: number } | { name: "finish_task"; taskId: number } | { name: "approve_public_reply"; platform: string; surface: "review" | "comment"; taskId: number } ``` ### TaskPerson Someone on the account, as Tasks names them. `interface TaskPerson` | Property | Type | | |---|---|---| | `email` | `string` | | | `name` | `string \| null` | | ### GiveTask A task to give: to someone on the account by email, or to the owner when `forEmail` is left out. `interface GiveTask` | Property | Type | | |---|---|---| | `detail` (optional) | `string` | | | `dueAt` (optional) | `string` | ISO time it is due. | | `forEmail` (optional) | `string` | | | `linkPath` (optional) | `string` | A hub path the task points at, e.g. /contacts/people/... | | `title` | `string` | | ### TaskRecord What happened to a given task (tasks.record()) or a decision (tasks.decisionRecord()): who gave or asked it, who passed it on, who closed or decided it, and how long each person had it. `interface TaskRecord` | Property | Type | | |---|---|---| | `kind` | `"task" \| "decision"` | | | `state` | `string` | | | `steps` | `TaskRecordStep[]` | | | `timings` | `{ handoffs: number; total: string \| null; totalMs: number \| null; untilFirstSeen: string \| null; untilFirstSeenMs: number \| null; withPeople: { ms: number; name: string; text: string }[] }` | | | `title` | `string` | | ### TaskRecordStep One step in a task's record, in order. `interface TaskRecordStep` | Property | Type | | |---|---|---| | `at` | `string` | | | `gap` | `string \| null` | How long since the step before, when it is worth showing ("29 min later"). | | `place` | `string \| null` | Where it happened (email, Telegram), when that was not the hub; otherwise null. | | `text` | `string` | What happened, in a sentence. | | `tone` | `"start" \| "move" \| "seen" \| "answer" \| "end"` | | ### DecisionBoard The decisions on the Tasks page, as the account's token sees them (read only). `interface DecisionBoard` | Property | Type | | |---|---|---| | `decided` | `DecidedDecision[]` | Decided in the last 14 days. | | `later` | `DecisionCard[]` | Put off with Not now. | | `others` | `DecisionCard[]` | With `scope: 'all'`: everything else waiting, to see. | | `toDecide` | `DecisionCard[]` | Waiting on a decision now. | | `waitingOnOthers` | `DecisionCard[]` | Said yes to and gone further up the line. | ### DecisionCard A decision waiting on someone, from tasks.decisions(): our words and numbers only, never a customer's message. `interface DecisionCard` | Property | Type | | |---|---|---| | `actions` | `string[]` | What may be pressed now. Always empty for an account token: deciding is a person's tap in the hub. | | `allowOther` | `boolean` | | | `assurance` | `"any" \| "signed_in"` | `signed_in`: only answered from a signed-in hub session. | | `body` | `string \| null` | The facts and what a yes does. | | `chain` | `string \| null` | What a yes does when it is above the person's limit ("your yes sends it to Sam to approve"). | | `context` | `string \| null` | Why it is asked. | | `createdAt` | `string` | | | `deadline` | `string \| null` | | | `decider` | `{ kind: "person" \| "owners"; label: string }` | Who decides it now: one person, or the owners. | | `draft` | `{ field: string } \| null` | A draft is read live, in the hub, by the person deciding; null when there is none. | | `dueAt` | `string \| null` | | | `expiresAt` | `string` | | | `facts` | `{ amountCents: number \| null; currency: string \| null; discountPct: number \| null; line: string \| null; newCustomer: boolean \| null; selfDeclared: boolean } \| null` | | | `firstSeenAt` | `string \| null` | | | `holdsWork` | `boolean` | Work waits behind it: a yes lets it carry on. | | `kind` | `"yes_no" \| "approve_draft" \| "pick_one" \| "pick_many" \| "input" \| "inform"` | | | `linkPath` | `string \| null` | A hub page for "Open", or null. | | `note` | `string \| null` | Why this caller cannot decide it. | | `options` | `{ key: string; label: string }[] \| null` | | | `priority` | `number` | 1 routine to 5 urgent. | | `procedure` | `{ ref: string; title: string } \| null` | | | `raisedBy` | `string` | Who raised it, in words. | | `role` | `{ department: { id: number; name: string; title: string } \| null; id: number; title: string } \| null` | The role the work is on, and its department. | | `snoozedUntil` | `string \| null` | | | `source` | `{ agentId: string; agentName: string; kind: "agent" } \| { kind: "person"; mid: number; name: string \| null } \| { kind: "workflow"; name: string \| null; workflowId: string }` | Who raised it: an agent, a person on the account, or an automation. | | `step` | `number` | | | `taskId` | `number` | Pass it to tasks.decisionRecord(). | | `title` | `string` | | ### DecidedDecision A decision made recently, from tasks.decisions(). `interface DecidedDecision` | Property | Type | | |---|---|---| | `answer` | `{ picks: string[] \| null; text: string \| null } \| null` | A person's ask: the answer they were given. | | `at` | `string` | | | `by` | `{ mid: number \| null; name: string \| null } \| null` | | | `kind` | `"yes_no" \| "approve_draft" \| "pick_one" \| "pick_many" \| "input" \| "inform"` | | | `raisedBy` | `string` | | | `state` | `"waiting" \| "approved" \| "rejected" \| "expired" \| "cancelled"` | | | `taskId` | `number` | | | `text` | `string` | What was decided, in a sentence. | | `title` | `string` | | ## Numbers ### Metric Something the contact list can be measured by, from metrics(). `interface Metric` | Property | Type | | |---|---|---| | `aggregation` | `string` | count for people, sum for a field. | | `category` | `string` | | | `key` | `string` | `people`, or a number field's key. | | `label` | `string` | | | `mapped` | `{ column_id: string; dataset_id: string; name: string }[]` | | | `synonyms` | `string[]` | Other words for it. | | `unit` | `string` | people, a currency such as AUD, minutes, or empty. | ### MetricSeries One metric over time, from series(). `interface MetricSeries` | Property | Type | | |---|---|---| | `aggregation` | `string` | | | `applied` | `Record & { anchor: string; defaults: string[]; timezone: string }` | What the hub assumed (defaults, time zone, the date people are counted by): say it back. | | `compare` | `{ delta_pct: number \| null; range: { from: string; to: string }; total: number \| null; trend: (number \| null)[] } \| null` | With compare: 'previous': the window before, and the change in percent. | | `coverage` | `{ anchor: string; from: string \| null; people: number; to: string \| null; with_anchor: number }` | How many people have the date the series counts by, and over what span. | | `grain` | `string` | | | `label` | `string` | | | `labels` | `string[]` | Each bucket's start (YYYY-MM-DD), in order. | | `metric` | `string` | | | `points` | `{ t: string; value: number \| null }[]` | | | `range` | `{ from: string; to: string }` | | | `total` | `number \| null` | The whole window, or null when nothing landed in it. | | `trend` | `(number \| null)[]` | Each bucket's value, in the same order. | | `unit` | `string` | | ### Dataset A dataset, from datasets(): today the contact list, `contact`. `interface Dataset` | Property | Type | | |---|---|---| | `columns` | `{ choices?: string[]; column_id: string; counts_people_by?: boolean; kind: "anchor" \| "measure" \| "dimension"; name: string; type: string; unit?: string }[]` | measure: can be counted or summed; dimension: can be grouped by; anchor: a date people can be counted by. | | `dataset_id` | `string` | | | `description` | `string` | | | `grain` | `string` | | | `kind` | `string` | | | `name` | `string` | | | `period_from` | `string \| null` | | | `period_to` | `string \| null` | | | `record_count` | `number` | | ## App people ### AppPerson Someone who can sign in to the account's apps, from appUsers. `interface AppPerson` | Property | Type | | |---|---|---| | `contact_id` | `string \| null` | Their record in Contacts, when linked. | | `created_at` (optional) | `string` | | | `disabled_at` | `string \| null` | When they were disabled; null while they can sign in. | | `email` | `string` | | | `id` | `string` | | | `last_sign_in_at` (optional) | `string \| null` | | | `role` | `string` | owner, staff, member or one of the account's own roles. | ### AssistantUsage One app's AI assistant this month, from assistantUsage(). `interface AssistantUsage` | Property | Type | | |---|---|---| | `knowledge_replies` | `number` | Replies that asked Knowledge: they count towards the account's Knowledge answers. | | `own_key` | `boolean` | Whether the account has its own AI key saved. | | `own_key_tokens` | `number` | Tokens spent on the account's own AI key. | | `replies` | `number` | Replies it sent, drafted or handed to a person. | | `runs` | `number` | Conversations it looked at. | | `since` | `string` | The start of the month counted (UTC). | | `tokens` | `number` | | ## Knowledge ### KnowledgeAnswer An answer from the account's Knowledge Base, from knowledge.ask(). `interface KnowledgeAnswer` | Property | Type | | |---|---|---| | `answer` | `string \| null` | The answer, with [n] markers for its sources; null unless status is ok. | | `grounded` | `boolean` | True only for an ok answer with sources. An ok answer with none came from the model, not the content. | | `noAnswerMessage` | `string \| null` | What the owner set the assistant to say when it has no answer. | | `session` | `string \| null` | Pass it back as `session` to keep the conversation going. | | `sources` | `{ kind: string \| null; locator: string \| null; ref: number; title: string \| null; url: string \| null }[]` | | | `status` | `string` | ok: answered; no_results: the content has no answer; error: the service failed, not the content. | ### KnowledgeFilters Narrow a Knowledge answer to part of the library: any of several values within one facet. `interface KnowledgeFilters` | Property | Type | | |---|---|---| | `author` (optional) | `string \| string[]` | | | `category` (optional) | `string \| string[]` | | | `doc_ids` (optional) | `string[]` | Documents, by their doc_id from a search hit. | | `kind` (optional) | `string \| string[]` | | | `person` (optional) | `string \| string[]` | | | `place` (optional) | `string \| string[]` | | | `topic` (optional) | `string \| string[]` | | | `year` (optional) | `number[]` | | ### KnowledgeSearchOptions What knowledge.search() takes. Facets take one value or several (any of them). `interface KnowledgeSearchOptions` | Property | Type | | |---|---|---| | `author` (optional) | `string \| string[]` | | | `category` (optional) | `string \| string[]` | | | `doc` (optional) | `string \| string[]` | Documents, by doc_id. | | `includeMedia` (optional) | `boolean` | Posters and addresses for audio and video hits. They expire in minutes: never store them. | | `kind` (optional) | `string \| string[]` | | | `limit` (optional) | `number` | 1 to 50. | | `offset` (optional) | `number` | | | `person` (optional) | `string \| string[]` | | | `place` (optional) | `string \| string[]` | | | `q` (optional) | `string` | | | `sort` (optional) | `"year:desc" \| "year:asc" \| "page:asc" \| "page:desc"` | Default relevance. | | `topic` (optional) | `string \| string[]` | | | `year` (optional) | `string \| string[]` | Four-digit years. | ### KnowledgeSearchResult What knowledge.search() returns. `interface KnowledgeSearchResult` | Property | Type | | |---|---|---| | `elapsed_ms` | `number` | | | `facets` | `Record>` | Counts for each facet's values, under the current filters. | | `highlight` | `{ post: string; pre: string }` | | | `hits` | `KnowledgeSearchHit[]` | | | `query` | `string` | | | `total` | `number` | | ### KnowledgeSearchHit One hit, from knowledge.search(). `interface KnowledgeSearchHit` | Property | Type | | |---|---|---| | `author` (optional) | `string` | | | `category` (optional) | `string` | | | `chapter` (optional) | `string` | | | `doc_id` | `string` | | | `kind` (optional) | `string` | | | `locator` | `string` | | | `page` (optional) | `number` | | | `people` (optional) | `string[]` | | | `places` (optional) | `string[]` | | | `poster_url` (optional) | `string` | | | `score` | `number \| null` | | | `snippet` | `string` | The matched text, with the words between highlight.pre and highlight.post. | | `source_id` (optional) | `string` | | | `title` | `string` | | | `topics` (optional) | `string[]` | | | `url` (optional) | `string` | | | `year` (optional) | `number` | | ### KnowledgeDataQuery A question for Your numbers, from knowledge.data(). Anything else the platform takes passes through. `interface KnowledgeDataQuery` | Property | Type | | |---|---|---| | `dataset` | `string` | The dataset's id, such as ds-builtin-google-analytics-daily. | | `dimensions` (optional) | `string[]` | | | `filters` (optional) | `Record[]` | | | `limit` (optional) | `number` | | | `measures` | `{ agg: string; column: string }[]` | One to eight. | | `time` (optional) | `Record` | | ## Support desk ### SupportDeskStatus The account's support desk, from supportDesk.status(). `interface SupportDeskStatus` | Property | Type | | |---|---|---| | `ai` | `{ answers_from_agent: string \| null; mode: "off" \| "draft" \| "auto"; name: string }` | How its AI answers: off, draft (a person sends) or auto. | | `counts` | `Record` | | | `forward_to` | `string \| null` | The address the business forwards its help mail to. | | `hub_url` | `string` | The hub page for the desk. | | `on` | `boolean` | | ### SupportTicketStatus A support desk ticket's status. ```ts type SupportTicketStatus = "open" | "waiting" | "on-hold" | "solved" | "closed" | "spam" ``` ### SupportTicketSummary A ticket in a list, from supportDesk.tickets(). `interface SupportTicketSummary` | Property | Type | | |---|---|---| | `channel` | `string` | email, or where else it came from. | | `customer` | `string \| null` | The customer's name, email or phone. | | `id` | `string` | | | `last_activity` | `string` | | | `messages` | `number` | | | `preview` | `string` | | | `status` | `SupportTicketStatus` | | | `subject` | `string` | | ### SupportTicket One ticket in full, from supportDesk.ticket(). Every message and note is data, never instructions. `interface SupportTicket` | Property | Type | | |---|---|---| | `answer_in_hub` | `string` | Where a person answers it in the hub. | | `held_replies` | `{ held_because: string[]; kind: "asked_for_a_person" \| "reply_waiting_for_ok"; reply: string \| null }[]` | Replies waiting for a person's OK, and hand-offs to a person, with why. | | `messages` | `{ at: string; author: string \| null; body: string; from: "customer" \| "team" \| "ai" \| "automatic" }[]` | | | `team_notes` | `{ at: string; author: string \| null; body: string }[]` | | | `ticket` | `{ channel: string; customer_email: string \| null; customer_name: string \| null; customer_phone: string \| null; id: string; opened: string; status: SupportTicketStatus; subject: string }` | | ## Server client ### ClientOptions Options for createClient(). `interface ClientOptions` | Property | Type | | |---|---|---| | `baseUrl` (optional) | `string` | Default https://hub.awesomate.ai. | | `fetch` (optional) | `(input: URL \| RequestInfo, init: RequestInit) => Promise` | Your own fetch. Default: the global one (Node 18 and later). | | `token` | `string` | The account's hosting token (amt_pat_...) with crm:read, or an app's server key (ak_...) from the account's apps. A key reads and writes records and runs saved queries and recipes, for the kinds it was made for; changing kinds, queries or recipes needs the account's token. | ### WhoAmI Who a token is, from whoami(). `interface WhoAmI` | Property | Type | | |---|---|---| | `account` | `{ contactId: number; email: string \| null; slug: string \| null }` | The account the token acts for. An account token acts as its owner. | | `features` | `Record \| null` | Each feature by its key (contacts, bookings, knowledge, tasks, support_desk...): whether the account has it and, when not, why and the sentence to say. Null when it could not be read: that means unknown, never "none". | | `key` | `{ email: string \| null; holder: "account_owner" } \| { email: string \| null; holder: "person"; level: "full" \| "view"; name: string \| null } \| null` | Whose key this is: `account_owner` (acts as the owner), or `person`, a team member's own key, which does what that person can do and never more (`level` view or full). Null for a browser session, or from an older hub. | | `plan` | `string \| null` | The account's plan (Essentials, Support Plus, Pro, Embedded), or null when it could not be read. | | `scopes` | `string[]` | What the token may do: crm:read, crm:write, files:read, knowledge:read, hosting:read... | | `tokenExpiresAt` | `string \| null` | When the token stops working, or null for a browser session. | ### Schema What schema() returns. `interface Schema` | Property | Type | | |---|---|---| | `access` (optional) | `"read" \| "write"` | With an app key: whether it can only read or can also write. | | `as_at` | `string` | | | `default_timezone` (optional) | `string` | With the account's token: the time zone dates are read in unless a query says otherwise. | | `kinds` | `SchemaKind[]` | | | `limits` (optional) | `{ default_limit: number; max_limit: number; statement_timeout_ms: number }` | With the account's token: the page size limits and the statement timeout. | ### SchemaKind One kind, from schema(). `interface SchemaKind` | Property | Type | | |---|---|---| | `columns` | `SchemaColumn[]` | | | `description` (optional) | `string` | What the owner wrote about it. | | `examples` | `{ ask: string; query: Record }[]` | Example questions with the query that answers each. | | `grammar` | `string` | The query grammar, in sentences. | | `hidden` | `{ not_readable_by_ai: number; note: string; sensitive: number }` | Fields left out: not marked readable by AI, or sensitive. `note` says so in a sentence. | | `kind` | `string` | | | `label` | `string` | | | `note` | `string` | | | `sections` (optional) | `{ columns: string[]; description?: string; key: string; label: string }[]` | The owner's sections, each with the columns in it: only sections holding a column this token can see. | ### SchemaColumn One column of a kind, from schema(). `interface SchemaColumn` | Property | Type | | |---|---|---| | `choices` (optional) | `string[]` | | | `description` (optional) | `string` | What the owner wrote about it. | | `field_type` (optional) | `string` | The field's own type (money, choice, email...), when it has one beyond the column type. | | `label` | `string` | | | `name` | `string` | | | `operators` | `string[]` | The where-clause operators it takes. | | `section` (optional) | `string` | The section it is in (a key of the kind's sections). | | `sortable` | `boolean` | Whether orderBy can use it (every column but a list). | | `type` | `"number" \| "boolean" \| "text" \| "date" \| "uuid" \| "timestamp" \| "text_array"` | | ### BusinessCatalogue Every detail a business can have, from businessCatalogue(). `interface BusinessCatalogue` | Property | Type | | |---|---|---| | `facts` | `{ core: boolean; group: string; key: string; label: string; level: "business" \| "brand"; max: number; type: string }[]` | | | `groups` | `Record` | Each group's title, by its id. | ### KindSpec A kind to define: a table an app keeps. `interface KindSpec` | Property | Type | | |---|---|---| | `access` (optional) | `KindAccess` | The app's access rules, set with the kind. Until a kind has rules, owner and staff can do everything with it and members nothing. | | `attributes` | `AttributeSpec[]` | | | `key` | `string` | | | `label` (optional) | `string` | | | `label_plural` (optional) | `string` | | | `links` (optional) | `{ key: string; label?: string; required?: boolean; to: string; to_label?: string }[]` | to is 'contact' or a kind already defined; each link reads back as _id. | ### AttributeSpec One attribute (column) of a kind. `interface AttributeSpec` | Property | Type | | |---|---|---| | `choices` (optional) | `string[]` | | | `key` | `string` | | | `label` (optional) | `string` | | | `readable_by_ai` (optional) | `boolean` | Default false. Only readable attributes reach query(), get() and the generated types. | | `required` (optional) | `boolean` | | | `sensitivity` (optional) | `"ordinary" \| "personal" \| "sensitive"` | | | `type` (optional) | `"number" \| "yes_no" \| "file" \| "email" \| "text" \| "long_text" \| "money" \| "date" \| "datetime" \| "duration" \| "choice" \| "choices" \| "phone" \| "address" \| "url"` | | | `visible_to` (optional) | `string[] \| null` | Which of the app's roles see this attribute (up to 10, lower case). Left out or null: everyone who can read the record. An empty list: none of the app's people. | ### KindDescription A kind as the account has defined it. `interface KindDescription` | Property | Type | | |---|---|---| | `access` | `KindAccess` | The kind's access rules. A kind with none set reads owner and staff `all`, for reading and writing. | | `attributes` | `(Required> & { choices: string[] \| null; visible_to: string[] \| null })[]` | | | `key` | `string` | | | `label` (optional) | `string` | | | `label_plural` (optional) | `string` | | | `links` (optional) | `{ key: string; label?: string; required?: boolean; to: string; to_label?: string }[]` | to is 'contact' or a kind already defined; each link reads back as _id. | | `storage` | `"plain" \| "tracked"` | | | `views` | `{ all: string; for_agents: string; for_app: string }` | | ### WriteOptions Options for write(). `interface WriteOptions` | Property | Type | | |---|---|---| | `id` (optional) | `string` | Change this record instead of creating one; values not given stay. | | `links` (optional) | `Record` | Set a link by its key (''), or end it (null). | ### SavedQuerySpec A saved query to store with saveQuery(). `interface SavedQuerySpec` | Property | Type | | |---|---|---| | `description` (optional) | `string` | | | `key` | `string` | | | `kind` | `string` | 'contact' or an app kind. | | `label` | `string` | | | `params` (optional) | `ParamSpec[]` | | | `spec` | `{ limit?: number; orderBy?: [string, "asc" \| "desc"][]; select?: string[]; where?: Record }` | | ### SavedQueryDescription A saved query as stored. `interface SavedQueryDescription` | Property | Type | | |---|---|---| | `description` | `string \| null` | | | `key` | `string` | | | `kind` | `string` | 'contact' or an app kind. | | `label` | `string` | | | `params` | `ParamSpec[]` | | | `spec` | `{ limit?: number; orderBy?: [string, "asc" \| "desc"][]; select?: string[]; where?: Record }` | | | `updated_at` | `string` | | ### ParamSpec One parameter of a saved query or recipe. `interface ParamSpec` | Property | Type | | |---|---|---| | `default` (optional) | `unknown` | | | `label` (optional) | `string` | | | `name` | `string` | | | `required` (optional) | `boolean` | | | `type` (optional) | `ParamType` | Default text. | ### ParamType The type of a saved query's or recipe's parameter. ```ts type ParamType = "text" | "number" | "date" | "datetime" | "boolean" | "uuid" | "text_list" | "number_list" ``` ### Param Where a caller's value goes in a saved query or a recipe. ```ts type Param = undefined ``` ### RecipeSpec A write recipe to store with saveRecipe(). `interface RecipeSpec` | Property | Type | | |---|---|---| | `description` (optional) | `string` | | | `key` | `string` | | | `label` | `string` | | | `params` (optional) | `ParamSpec[]` | | | `steps` | `RecipeStep[]` | | ### RecipeStep One step of a write recipe: write a record, or archive one. ```ts type RecipeStep = { as?: string; data?: Record; id?: Param | { $step: string }; kind: string; links?: Record; op: "write_record" } | { id: Param | { $step: string }; kind: string; op: "archive_record" } ``` ### RecipeDescription A write recipe as stored. `interface RecipeDescription` | Property | Type | | |---|---|---| | `description` | `string \| null` | | | `key` | `string` | | | `label` | `string` | | | `params` | `ParamSpec[]` | | | `run_by` | `string[]` | The app roles that may run it from the app (empty: only your server, with the account's token or an app key). The owner sets them in the hub or with Claude Code. | | `steps` | `RecipeStep[]` | | | `updated_at` | `string` | | ### RecipeResult What a recipe run wrote. `interface RecipeResult` | Property | Type | | |---|---|---| | `ids` | `Record` | The id each step named with `as` wrote. | | `records` | `string[]` | Every record the recipe wrote or archived, in step order. | ### KindAccess Who among an app's signed-in people may read and change a kind's records, by role. Each rule is `all`, `none`, `own` (records they created) or `linked:` (records linked to them, such as `linked:customer` or `linked:job.customer`); join several with `|`. A role left out gets none. `interface KindAccess` | Property | Type | | |---|---|---| | `read` (optional) | `Record` | | | `write` (optional) | `Record` | | ### SeriesEvent What happened to someone, for recordEvent(). `event` is lower case: letters, digits and `_ . : -`, up to 64 characters ("job_paid", "quote_sent"). `interface SeriesEvent` | Property | Type | | |---|---|---| | `email` | `string` | Whose event it is. Found in Contacts, or added. | | `event` | `string` | | | `firstName` (optional) | `string` | Used only when the person is added to Contacts by this event. | | `key` (optional) | `string` | Your own id for the event (1 to 200 characters). Sending the same key again records it once. | | `lastName` (optional) | `string` | | | `occurredAt` (optional) | `string \| Date` | When it happened. Default now. | | `record` (optional) | `string` | The quote, job or booking it is about (1 to 120 characters). A series that runs per record keys on it. | ### BusinessIdentity The business, as business() returns it. `interface BusinessIdentity` | Property | Type | | |---|---|---| | `account` | `string` | The account's slug. | | `completeness` | `{ core_set: number; core_total: number; missing_core: string[] }` | The eight core details every business should have, and which are missing. | | `groups` | `{ facts: BusinessFact[]; id: string; title: string }[]` | The details, grouped for reading. | | `how_to_change` | `string` | Where the owner changes these details, in words. | | `name` | `string \| null` | The business's name, when known. | | `sources` | `Record<"confirmed_details" \| "saved_details" \| "account_details" \| "website_research", string>` | Whether each source could be read: 'read', 'empty' or 'unavailable'. | ### BusinessFact One detail about the business. `interface BusinessFact` | Property | Type | | |---|---|---| | `key` | `string` | | | `label` | `string` | | | `source` | `string` | Where it came from, in words: "confirmed by the owner", "added by a team member", "website research"... | | `status` | `"confirmed" \| "suggestion"` | confirmed: in use by agents and automations. suggestion: waiting for the owner. | | `value` | `string` | | ### BusinessSuggestion A detail waiting for the owner's yes. `interface BusinessSuggestion` | Property | Type | | |---|---|---| | `id` | `number` | | | `key` | `string` | | | `recordedAt` | `string` | | | `sourceKind` | `string` | website, research, import, team_member, staff, upload, legacy_variables... | | `sourceRef` | `string \| null` | | | `value` | `string` | | ### BusinessDocumentKind The business's long documents, by kind. ```ts type BusinessDocumentKind = "brand_guide" | "voice_guide" | "website_brand" | "business_summary" ``` ### BusinessDocumentSummary One of the business's documents, without its text (businessDocuments()). `interface BusinessDocumentSummary` | Property | Type | | |---|---|---| | `chars` | `number` | How long the text is, in characters. | | `kind` | `BusinessDocumentKind` | | | `label` | `string` | What the owner sees it called in the hub ("Voice and style guide"). | | `recordedAt` | `string` | | | `sourceKind` | `string` | Where this version came from: the owner, the onboarding agent, a chat brought in, a file. | | `version` | `number` | Goes up by one each time it is saved again. | ### BusinessDocument One of the business's documents with its text (businessDocument()). `interface BusinessDocument` | Property | Type | | |---|---|---| | `body` | `string` | The document itself, usually Markdown. The owner's own words. | | `chars` | `number` | How long the text is, in characters. | | `kind` | `BusinessDocumentKind` | | | `label` | `string` | What the owner sees it called in the hub ("Voice and style guide"). | | `recordedAt` | `string` | | | `sourceKind` | `string` | Where this version came from: the owner, the onboarding agent, a chat brought in, a file. | | `version` | `number` | Goes up by one each time it is saved again. | ### BusinessMap The business map, as businessMap() returns it. No email addresses. `interface BusinessMap` | Property | Type | | |---|---|---| | `business` | `{ name: string \| null; ownerName: string }` | | | `counts` | `{ departmentsWithHelpers: number; helpers: number; people: number; placed: number; roles: number }` | | | `departments` | `BusinessMapDepartment[]` | | | `gaps` | `{ action: { label: string; path: string } \| null; department: number \| null; detail: string; kind: "procedure_first" \| "no_helper" \| "role_open" \| "path_missing" \| "path_unowned" \| "owner_everywhere" \| "unplaced"; title: string }[]` | What is missing, most important first. | | `path` | `{ steps: BusinessMapPathStep[]; stored: boolean; template: { key: string; label: string } \| null }` | How a customer moves through the business. stored false: suggested for its kind of business, not set by the owner yet. | | `people` | `{ access: "owner" \| "full" \| "view" \| null; fromTeamAccess: boolean; id: number \| null; kind: "owner" \| "staff" \| "contractor" \| "adviser" \| null; location: string \| null; name: string }[]` | access: what the person may do in the hub (owner, full or view). | | `quarter` | `string` | This quarter, '2026-Q4' (UTC): the one the map shows priorities for. | | `settings` | `{ adviser: string \| null; oneBrainCategory: string \| null; runsWeek: string \| null }` | oneBrainCategory: the 1Brain category this business is, once linked. | | `stored` | `boolean` | false: the owner has not started the map, so it is worked out from what the account runs. | | `unavailable` | `string[]` | Sources that could not be read just now: a missing helper may simply not have been read. | | `unplaced` | `(BusinessMapHelper & { placedBy: string })[]` | Helpers we could not place on a department. | | `version` | `2` | | ### BusinessMapDepartment One of the seven departments, in board order (7 first). `interface BusinessMapDepartment` | Property | Type | | |---|---|---| | `helpers` | `(BusinessMapHelper & { placedBy: string })[]` | Helping here but not yet put on a role, with how they were placed, in words. | | `ideas` | `{ label: string; path?: string; status: "live" \| "library" \| "coming" \| "building" \| "planned"; what: string }[]` | | | `name` | `string` | | | `no` | `1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7` | | | `noRoleYet` | `{ id: number; name: string }[]` | People on the map who hold no role yet. They sit in Form (department 1) for display; empty for every other department. | | `numbers` | `{ kind: "lead" \| "result"; label: string }[]` | | | `oneBrainDepartments` | `string[]` | 1Brain Departments whose procedures belong in this department. | | `purpose` | `string` | | | `question` | `string` | | | `roles` | `BusinessMapRole[]` | | | `runBy` | `{ byDefault: boolean; name: string }` | Who runs it. byDefault: nobody else has been given it, so the owner does. | | `stage` | `"survive" \| "grow" \| "scale"` | | | `subDepartments` | `BusinessMapSubDepartment[]` | | | `verb` | `string` | Envision, Form, Promise, Balance, Fulfil, Refine, Share. | ### BusinessMapSubDepartment A sub-department and who runs it: the holder of the role that heads it, else whoever runs the department (byDefault). `interface BusinessMapSubDepartment` | Property | Type | | |---|---|---| | `gloss` | `string` | | | `headRole` | `{ id: number; slug: string; title: string } \| null` | | | `name` | `string` | | | `no` | `number` | | | `runBy` | `{ byDefault: boolean; name: string }` | | ### BusinessMapRole A role on the map. `slug` is its stable name for routing work: "whoever holds quotes". `interface BusinessMapRole` | Property | Type | | |---|---|---| | `headsDepartment` | `boolean` | Runs its department: above its sub-departments, so subDepartmentNo is null (the owner's role excepted). | | `headsSubDepartment` | `boolean` | Runs its sub-department: the person responsible for it, and for its KPIs. | | `helpers` | `(BusinessMapHelper & { exceptions: string \| null; id: number; level: BusinessMapLevel \| null; minutesSavedPerRun: number \| null; supervisor: string })[]` | | | `holders` | `{ accountable: boolean; membershipId: number; name: string; timeSharePct: number \| null }[]` | | | `id` | `number` | | | `isOwnerRole` | `boolean` | | | `mission` | `string \| null` | | | `priorities` | `BusinessMapPriority[]` | Quarterly priorities on this role, every quarter. | | `proceduresTag` | `string` | The tag this role's procedures carry in 1Brain, such as 'job:quotes'. The map keeps no procedure text. | | `reportsTo` | `{ id: number; title: string } \| null` | | | `responsibilities` | `{ exceptions: string \| null; level: BusinessMapLevel; text: string }[]` | | | `slug` | `string` | | | `state` | `"open" \| "active" \| "hire_next"` | active: someone holds it. open: nobody does. hire_next: the owner's next hire. | | `subDepartmentNo` | `number \| null` | | | `title` | `string` | | ### BusinessMapRoleSummary One role in businessMapRoles(). `interface BusinessMapRoleSummary` | Property | Type | | |---|---|---| | `department` | `{ name: string; no: number; verb: string }` | | | `holder` | `string \| null` | The accountable holder, else the first; null when nobody holds it. | | `slug` | `string` | | | `state` | `"open" \| "active" \| "hire_next"` | | | `title` | `string` | | ### BusinessMapRoleDetail One role, as businessMapRole() returns it. `interface BusinessMapRoleDetail` | Property | Type | | |---|---|---| | `department` | `{ name: string; no: number; verb: string }` | | | `headsDepartment` | `boolean` | | | `headsSubDepartment` | `boolean` | | | `helpers` | `{ exceptions: string \| null; instructionLine: string; kind: "agent" \| "bundle" \| "template" \| "build" \| "external"; label: string; level: BusinessMapLevel \| null; levelLabel: string \| null; ref: string; supervisor: string }[]` | | | `holder` | `string \| null` | | | `holders` | `{ accountable: boolean; name: string; timeSharePct: number \| null }[]` | | | `mission` | `string \| null` | | | `pathSteps` | `{ handoffRule: string \| null; label: string; position: number }[]` | | | `priorities` | `{ dueOn: string \| null; owner: string \| null; quarter: string; status: "open" \| "on_track" \| "off_track" \| "done" \| "not_done"; title: string }[]` | This quarter's and next quarter's. | | `procedures` | `{ tag: string; where: "1Brain" }` | | | `reportsTo` | `{ slug: string \| null; title: string } \| null` | | | `responsibilities` | `{ exceptions: string \| null; level: BusinessMapLevel; levelLabel: string; text: string }[]` | | | `slug` | `string` | | | `state` | `"open" \| "active" \| "hire_next"` | | | `title` | `string` | | ### BusinessMapHelper Something that helps: an agent, a library install, an automation Awesomate built, or a tool outside Awesomate. `interface BusinessMapHelper` | Property | Type | | |---|---|---| | `kind` | `"agent" \| "bundle" \| "template" \| "build" \| "external"` | | | `label` | `string` | | | `ref` | `string` | | ### BusinessMapLevel How far someone, or an agent, decides alone: 1 find out, 2 suggest options, 3 recommend and wait, 4 do it then tell, 5 report only the exceptions. ```ts type BusinessMapLevel = 1 | 2 | 3 | 4 | 5 ``` ### BusinessMapPathStep One step of the customer's path. `interface BusinessMapPathStep` | Property | Type | | |---|---|---| | `departmentNo` | `1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7` | | | `handoffRule` | `string \| null` | Where this step hands over to the next, in the owner's words. | | `label` | `string` | | | `owner` | `{ byDefault: boolean; name: string }` | Who looks after it: the role's holder, else whoever runs the department (byDefault). | | `position` | `number` | | | `role` | `{ id: number; slug: string; title: string } \| null` | The role that looks after the step, when the owner named one. | | `verb` | `string` | | ### BusinessMapPriority A quarterly priority on a role: one of the few things it must move this quarter. `interface BusinessMapPriority` | Property | Type | | |---|---|---| | `dueOn` | `string \| null` | 'YYYY-MM-DD'. | | `id` | `number` | | | `owner` | `string \| null` | | | `ownerMembershipId` | `number \| null` | | | `quarter` | `string` | '2026-Q4'. | | `status` | `"open" \| "on_track" \| "off_track" \| "done" \| "not_done"` | | | `title` | `string` | | ### BusinessMapV1 The business map in its first shape, as businessMapV1() returns it. `interface BusinessMapV1` | Property | Type | | |---|---|---| | `business` | `{ name: string \| null; ownerName: string }` | | | `counts` | `{ divisionsWithHelpers: number; helpers: number; jobs: number; people: number; placed: number }` | | | `divisions` | `BusinessMapDivision[]` | | | `gaps` | `{ action: { label: string; path: string } \| null; detail: string; division: number \| null; kind: "procedure_first" \| "no_helper" \| "path_missing" \| "path_unowned" \| "owner_everywhere" \| "unplaced" \| "job_open"; title: string }[]` | | | `path` | `{ steps: BusinessMapPathStepV1[]; stored: boolean; template: { key: string; label: string } \| null }` | | | `people` | `{ fromTeamAccess: boolean; id: number \| null; kind: "owner" \| "staff" \| "contractor" \| "adviser" \| null; location: string \| null; name: string; role: "owner" \| "full" \| "view" \| null }[]` | role: the person's access. | | `quarter` | `string` | | | `settings` | `{ adviser: string \| null; oneBrainCategory: string \| null; runsWeek: string \| null }` | | | `stored` | `boolean` | | | `unavailable` | `string[]` | | | `unplaced` | `(BusinessMapHelper & { placedBy: string })[]` | | ### BusinessMapDivision A division on the v1 map (a department). `interface BusinessMapDivision` | Property | Type | | |---|---|---| | `departments` | `{ gloss: string; headJob: { id: number; slug: string; title: string } \| null; name: string; no: number; runBy: { byDefault: boolean; name: string } }[]` | The sub-departments. | | `helpers` | `(BusinessMapHelper & { placedBy: string })[]` | | | `ideas` | `{ label: string; path?: string; status: "live" \| "library" \| "coming" \| "building" \| "planned"; what: string }[]` | | | `jobs` | `BusinessMapJob[]` | | | `name` | `string` | | | `no` | `1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7` | | | `noHatYet` | `{ id: number; name: string }[]` | | | `numbers` | `{ kind: "lead" \| "result"; label: string }[]` | | | `oneBrainDepartments` | `string[]` | | | `purpose` | `string` | | | `question` | `string` | | | `runBy` | `{ byDefault: boolean; name: string }` | | | `stage` | `"survive" \| "grow" \| "scale"` | | | `verb` | `string` | | ### BusinessMapJob A job on the v1 map (a role). `interface BusinessMapJob` | Property | Type | | |---|---|---| | `departmentNo` | `number \| null` | The sub-department. | | `headsDepartment` | `boolean` | Runs its department (a sub-department). | | `headsDivision` | `boolean` | Runs its division (a department). | | `helpers` | `(BusinessMapHelper & { exceptions: string \| null; id: number; level: BusinessMapLevel \| null; minutesSavedPerRun: number \| null; supervisor: string })[]` | | | `holders` | `{ accountable: boolean; membershipId: number; name: string; timeSharePct: number \| null }[]` | | | `id` | `number` | | | `isOwnerJob` | `boolean` | | | `mission` | `string \| null` | | | `priorities` | `BusinessMapPriority[]` | | | `proceduresTag` | `string` | | | `reportsTo` | `{ id: number; title: string } \| null` | | | `responsibilities` | `{ exceptions: string \| null; level: BusinessMapLevel; text: string }[]` | | | `slug` | `string` | | | `state` | `"open" \| "active" \| "hire_next"` | | | `title` | `string` | | ### BusinessMapPathStepV1 One step of the customer's path on the v1 map. `interface BusinessMapPathStepV1` | Property | Type | | |---|---|---| | `divisionNo` | `1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7` | The department. | | `handoffRule` | `string \| null` | | | `job` | `{ id: number; slug: string; title: string } \| null` | The role. | | `label` | `string` | | | `owner` | `{ byDefault: boolean; name: string }` | | | `position` | `number` | | | `verb` | `string` | | ## Bookings ### BookingsClientOptions Options for createBookingsClient(). `interface BookingsClientOptions` | Property | Type | | |---|---|---| | `baseUrl` (optional) | `string` | Default https://hub.awesomate.ai. | | `fetch` (optional) | `(input: URL \| RequestInfo, init: RequestInit) => Promise` | Your own fetch. Default: the global one. | | `key` | `string` | The booking key (bk_...) from Contacts, Bookings, On your website. Public: it works only on the sites it lists. | ### BookableService Something the business offers to book, with the calendars (people, rooms) it is booked with. `interface BookableService` | Property | Type | | |---|---|---| | `calendars` | `{ key: string; name: string; timezone: string }[]` | | | `cancel_cutoff_hours` | `number` | How many hours before the start a customer can still cancel or move online. | | `capacity` | `number` | 1 for one person at a time; more for a class several people join at one start. | | `description` | `string` | | | `intake` | `BookingQuestion[]` | | | `key` | `string` | | | `location` | `string` | | | `minutes` | `number` | How long it lasts. | | `name` | `string` | | | `price_text` | `string` | The price as the business wrote it. Shown, never charged. | ### BookingQuestion One question a service asks when someone books. `interface BookingQuestion` | Property | Type | | |---|---|---| | `key` | `string` | | | `label` | `string` | | | `options` (optional) | `string[]` | The choices, for select and multiselect. | | `required` (optional) | `boolean` | | | `type` | `"select" \| "text" \| "url" \| "textarea" \| "multiselect"` | | ### OpenTime A time that can be booked. `interface OpenTime` | Property | Type | | |---|---|---| | `calendar` | `string` | The calendar it is with. | | `calendarName` | `string` | | | `end` | `string` | | | `joins` | `boolean` | True when this start joins a class someone has already booked. | | `seatsLeft` | `number` | Places left: 1 for a one-person service, more while a class has room. | | `start` | `string` | | ### BookingRequest What book() needs. Take `startsAt` and `calendar` from an OpenTime. `interface BookingRequest` | Property | Type | | |---|---|---| | `answers` (optional) | `Record` | Answers to the service's questions, by question key. | | `calendar` | `string` | | | `email` | `string` | | | `idempotencyKey` (optional) | `string` | One per attempt, so a retry after a dropped connection returns the same booking instead of making a second. Make a new one for each new booking. | | `name` | `string` | | | `phone` (optional) | `string` | | | `seats` (optional) | `number` | Places, for a class. Default 1. | | `service` | `string` | | | `startsAt` | `string` | | ### BookingResult A booking just made. `interface BookingResult` | Property | Type | | |---|---|---| | `bookingId` | `string` | | | `created` | `boolean` | False when idempotencyKey matched a booking already made. | | `endsAt` | `string` | | | `manageUrl` | `string` | The customer's own page to change or cancel, the same link their email carries. | | `startsAt` | `string` | | ### ManagedBooking One booking, as its customer's link shows it. Names no person. `interface ManagedBooking` | Property | Type | | |---|---|---| | `bookingId` | `string` | | | `canCancel` | `boolean` | Whether it can still be cancelled or moved online (the service's cutoff has not passed). | | `canMove` | `boolean` | | | `changesCloseAt` | `string` | | | `endsAt` | `string` | | | `location` | `string` | | | `priceText` | `string` | The price as the business wrote it. Shown, never charged. | | `seats` | `number` | | | `service` | `string \| null` | | | `startsAt` | `string` | | | `status` | `"cancelled" \| "confirmed" \| "completed" \| "no_show"` | | | `timezone` | `string` | The calendar's IANA zone, for showing the times. | | `with` | `string \| null` | The calendar's name: who or what it is with. | ### BookingPageLook How the business looks, for a booking page in its colours. Each is null when the business has not set it. `interface BookingPageLook` | Property | Type | | |---|---|---| | `colour` | `string \| null` | Its main colour, as #rrggbb. | | `logo` | `string \| null` | Its logo's address (https). | | `website` | `string \| null` | The business's website (https). | ### StaffBooking A booking as the business's staff see it (db.bookings), with its customer. `interface StaffBooking` | Property | Type | | |---|---|---| | `answers` | `Record \| null` | The customer's answers to the service's questions, by question key. | | `bookingId` | `string` | | | `calendar` | `string` | The calendar's key, and its name when the booking was made. | | `calendarName` | `string \| null` | | | `cancelledAt` | `string \| null` | | | `contactId` | `string` | The customer's contact id in Contacts. | | `createdAt` | `string` | | | `customer` | `{ email: string \| null; name: string \| null; phone: string \| null } \| null` | The customer, or null when they have since been removed from Contacts. | | `endsAt` | `string` | | | `seats` | `number` | | | `service` | `string` | The service's key, and its name when the booking was made. | | `serviceName` | `string \| null` | | | `source` | `string \| null` | How it was made: website, hub, phone, agent or api. | | `startsAt` | `string` | | | `status` | `"cancelled" \| "confirmed" \| "completed" \| "no_show"` | | ### StaffBookingRequest What db.bookings.book() needs. Take `startsAt` and `calendar` from an OpenTime. `interface StaffBookingRequest` | Property | Type | | |---|---|---| | `answers` (optional) | `Record` | Answers to the service's questions, by question key. | | `calendar` | `string` | | | `email` | `string` | The customer, found in Contacts by email or added. | | `firstName` (optional) | `string` | | | `idempotencyKey` (optional) | `string` | One per booking, kept across retries, so a retry returns the same booking. | | `lastName` (optional) | `string` | | | `notify` (optional) | `boolean` | false: no email to the customer (the calendar's notice address still hears). Default true. | | `outsideHours` (optional) | `boolean` | Book outside the calendar's hours and notice. Overlap, seats and the daily limit still apply. | | `phone` (optional) | `string` | | | `seats` (optional) | `number` | Places, for a class. Default 1. | | `service` | `string` | | | `startsAt` | `string` | | ### BookingSetup How bookings are set up, from bookings.setup(). `interface BookingSetup` | Property | Type | | |---|---|---| | `calendars` | `BookingCalendar[]` | | | `enabled` | `boolean` | | | `google` | `{ available: boolean }` | | | `links` | `{ account_email: string \| null; busy_calendars: string[]; calendar_key: string; can_choose: boolean; connected_at: string; last_error: string \| null; provider: string; status: "active" \| "broken" }[]` | Calendars connected to someone's own Google or Outlook calendar. | | `microsoft` | `{ available: boolean }` | | | `self_serve` | `{ per_month: number \| null; this_month: number }` | Online bookings this month against the plan's (null per_month: no limit). | | `services` | `BookingServiceSetup[]` | | ### BookingCalendar A calendar bookings are taken on (a person, a room), from bookings.setup(). `interface BookingCalendar` | Property | Type | | |---|---|---| | `active` | `boolean` | | | `buffer_minutes` | `number` | | | `granularity_minutes` | `number` | | | `has_busy_feed` | `boolean` | Busy times come from the person's own calendar feed. | | `hours` | `Partial>` | Open hours by weekday (mon to sun), each a list of [start, end] times ("09:00"). | | `key` | `string` | | | `max_per_day` | `number \| null` | | | `min_notice_minutes` | `number` | | | `name` | `string` | | | `notify_email` | `string \| null` | Where its booking notices go. | | `timezone` | `string` | | ### BookingServiceSetup A service as it is set up, from bookings.setup(). `interface BookingServiceSetup` | Property | Type | | |---|---|---| | `active` | `boolean` | | | `calendars` | `string[]` | The calendars that take it, by key. | | `cancel_cutoff_hours` | `number` | | | `capacity` | `number` | | | `description` | `string` | | | `intake` | `BookingQuestion[]` | | | `key` | `string` | | | `location` | `string` | | | `minutes` | `number` | | | `name` | `string` | | | `price_text` | `string` | | | `sort` | `number` | | ### manageTokenFrom The token in a booking's manage link (`/booking?t=...`), or null. ```ts manageTokenFrom(url: string): string | null ``` ## Package ### VERSION This package's version, sent to the hub with every server call. ```ts const VERSION: "0.27.0" ``` --- # Changelog Source: https://hub.awesomate.ai/docs/sdk/changelog/ What changed in each version of @awesomate/sdk. Every version of `@awesomate/sdk`, newest first. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Until 1.0, a minor version can change behaviour; each entry says when it does. ## [0.27.0] - 2026-10-09 ### Added - `db.whoami()` returns `key`: whose key it is. `account_owner` acts as the owner; `person` is a team member's own key, which does only what that person can do in the hub (`level` view or full). ## [0.26.0] - 2026-10-09 ### Added - `db.whoami()`: the account, its plan, the token's scopes and expiry, and which features the account has, each with the sentence to say when it does not. See [Server and n8n](https://hub.awesomate.ai/docs/sdk/guides/server-and-n8n/). - `db.metrics()`, `db.series(metric, options)`, `db.datasets()` and `db.dataset(id)`: what the contact list can be measured by, one figure over time, and the dataset aggregate() reads. See [Numbers](https://hub.awesomate.ai/docs/sdk/guides/metrics/). - `db.appUsers.list()`, `invite(email, { role })` and `update(id, { role, disabled, contactId })`: the people who sign in to the account's apps. Changes need Pro. See [App people](https://hub.awesomate.ai/docs/sdk/guides/app-people/). - `db.bookings.setup()`: the calendars and services, so openTimes() and book() have keys to use. - `db.tasks.decisions({ scope })` and `db.tasks.decisionRecord(taskId)`: the decisions on the Tasks page, read only. Deciding stays a person's tap in the hub. - `db.supportDesk.status()`, `tickets({ status, limit })` and `ticket(id)`: the account's own support desk, read only. See [Support desk](https://hub.awesomate.ai/docs/sdk/guides/support-desk/). - `db.knowledge.ask(question, { session, filters })`, `search(options)` and `data(query)`: a cited answer, instant search and Your numbers, from a server. See [Knowledge from a server](https://hub.awesomate.ai/docs/sdk/guides/knowledge/). - `db.addAttribute(kind, attribute)` and `db.archiveAttribute(kind, key)`, so a kind defined from code can be changed from code. - `db.assistantUsage(appId)`, `db.businessCatalogue()`, `db.files.extract(path)` and `db.files.capabilities()`. - `verifyAppToken(token, { appId })`: check an app's access token on your own server against the hub's keys. `app.auth.me()`: the signed-in person read fresh, with the app's settings. - `AwesomateError` now carries `reason`, `feature`, `missingScopes`, `upgrade` and `requiredPlan` when the hub sent them. Type `AwesomateErrorDetails`. - Fields the hub already sent: `TaskCard.type`, `openLabel`, `ownerOnly`, `action`, `passedBy` and `task.raisedBy`; `schema()` now has its full type (`Schema`): each kind's description, sections, hidden fields, grammar and examples, each column's description, section, field type, choices and whether it sorts, and `limits`, `default_timezone` or (for an app key) `access`. - Types `WhoAmI`, `Schema`, `SchemaKind`, `SchemaColumn`, `Metric`, `MetricSeries`, `Dataset`, `AppPerson`, `AssistantUsage`, `BusinessCatalogue`, `BookingSetup`, `BookingCalendar`, `BookingServiceSetup`, `DecisionBoard`, `DecisionCard`, `DecidedDecision`, `TaskCardAction`, `SupportDeskStatus`, `SupportTicketStatus`, `SupportTicketSummary`, `SupportTicket`, `KnowledgeAnswer`, `KnowledgeFilters`, `KnowledgeSearchOptions`, `KnowledgeSearchResult`, `KnowledgeSearchHit`, `KnowledgeDataQuery`, `FilesCapabilities`, `AppMe` and `AppTokenClaims`. ### Changed - Three new error codes. A refusal that used to be `forbidden` is now `feature_unavailable` (the account does not have the feature), `upgrade_required` (a plan floor, including the hub's `plan_required`) or `scope_missing` (the token lacks a scope). If you branched on `code === 'forbidden'` for these, branch on the new codes; `serverCode` keeps the hub's code. - Every way the hub says "switched off in Privacy" is now `consent_blocked`: `consent_required`, `consent_off`, `consent_missing` and Knowledge's `consent_required: true` joined `consent_denied` and `files_consent_off`. - A refusal whose `error` is a code and whose `message` is the sentence (Knowledge's shape) now has the sentence as `message` and the code as `serverCode` (it was empty before). - An app key's call that met `conflict` now retries on its own route instead of failing `forbidden`. - `verifyAppToken` refetches the hub's keys for an unknown key id at most every 30 seconds, and a non-JSON key answer is `unavailable`. - `tasks.record()` returns `TaskRecord & { kind: 'task' }`; `TaskRecord.kind` is `'task' | 'decision'`. - The bookings client's fallback message for a refusal with no words is now "The hub answered .", as the other clients'. ### Fixed - An app key's `schema()` now carries the owner's descriptions and sections, as the account token's does (the hub left them out). ## [0.25.0] - 2026-10-07 ### Added - `db.recordEvent({ event, email, record?, occurredAt?, key?, firstName?, lastName? })`: tell Contacts something happened to someone, so any email series waiting for that event starts or stops. Never changes consent; the same `key` is recorded once. See [Email series events](https://hub.awesomate.ai/docs/sdk/guides/email-series-events/). - `db.bookings`: the account's bookings for staff screens: `list()`, `get()`, `openTimes()`, `book()`, `cancel()`, `move()` and `outcome()`. Writes need Support Plus and Bookings on the account. See [Bookings, Staff screens](https://hub.awesomate.ai/docs/sdk/guides/bookings/). - `db.businessDocuments()` and `db.businessDocument(kind)`: the business's brand guide, voice guide, brand from its website and business summary. - `db.tasks.bringBack(id)`, `db.tasks.record(id)` and `db.tasks.cancelNotNow(key)`. - `db.files.trash()` and `db.files.emptyTrash({ olderThanDays })`. - Types `StaffBooking`, `StaffBookingRequest`, `BookingPageLook`, `SeriesEvent`, `BusinessDocument`, `BusinessDocumentSummary`, `BusinessDocumentKind`, `KindAccess`, `TaskRecord`, `TaskRecordStep` and `TrashEntry`. - Fields the hub already sent: `TaskBoard.canChooseScope`, `canGive`, `unavailable`, `hasTeam` and `decided[].canBringBack`; `BookableService.cancel_cutoff_hours`; `ManagedBooking.priceText`; `RecipeDescription.run_by`; `KindDescription.access` and each attribute's `visible_to`; `views.for_app`. - `AttributeSpec.visible_to` and `KindSpec.access`, which `defineKind()` already accepted. - `bookings.booking(token)` now also returns `look` (the business's website, logo and colour), which the hub already sent. ### Changed - `get()` on both clients answers null, without a request, for an id that is not a UUID. It used to send the request and throw a validation error, though its doc said null. - `bookings.book()`, `cancel()` and `move()` now name every `conflict` reason the hub can give: `too_many_seats`, `too_many_open`, `monthly_limit` and `too_late` were missing. ## [0.24.0] - 2026-10-06 ### Changed - `db.businessMap()` now returns the map in the words the owner sees in the hub, from `/api/my-business/v2/map`. This changes its shape: rename as below, or call `db.businessMapV1()` for the old shape while you move. | Before (v1) | Now | |---|---| | `divisions` | `departments` | | `division.jobs` | `department.roles` | | `division.departments` | `department.subDepartments` | | `departments[].headJob` | `subDepartments[].headRole` | | `division.noHatYet` | `department.noRoleYet` | | `job.isOwnerJob` | `role.isOwnerRole` | | `job.headsDivision` | `role.headsDepartment` | | `job.headsDepartment` | `role.headsSubDepartment` | | `job.departmentNo` | `role.subDepartmentNo` | | `path.steps[].divisionNo` | `path.steps[].departmentNo` | | `path.steps[].job` | `path.steps[].role` | | `people[].role` | `people[].access` | | `gaps[].division` | `gaps[].department` | | gap kind `job_open` | `role_open` | | `counts.divisionsWithHelpers`, `counts.jobs` | `counts.departmentsWithHelpers`, `counts.roles` | ### Added - `db.businessMapRoles()`: every role in one line each, and `db.businessMapRole(slug)`: one role with the line each helper's instructions carry. - Types `BusinessMapRole`, `BusinessMapDepartment`, `BusinessMapSubDepartment`, `BusinessMapRoleSummary`, `BusinessMapRoleDetail`. `BusinessMap.settings.oneBrainCategory`, which the hub already sent. ### Deprecated - `db.businessMapV1()` and the types `BusinessMapV1`, `BusinessMapDivision`, `BusinessMapJob`, `BusinessMapPathStepV1`. The v1 routes are removed once they have gone 30 days unused, and not before 2026-11-06. ## [0.23.0] - 2026-10-06 ### Added - `db.businessMap()`: each division's `noHatYet`, the people on the map who wear no hat yet. They sit in Form (division 1) for display, so the list is empty for every other division. ## [0.22.0] - 2026-10-06 ### Added - `db.businessMap()`: each hat's quarterly `priorities` (quarter, title, owner, status, due date) and the map's `quarter` (this quarter, UTC). Type `BusinessMapPriority`. ## [0.21.0] - 2026-10-05 ### Added - `db.businessMap()`: each job's `headsDepartment`, and each division's `departments[].runBy` and `headJob`, so a department-level KPI can be shown against the person responsible for the department. A job that runs its division (`headsDivision`) now sits above its departments and has no `departmentNo`, except the owner's own job. ## [0.20.0] - 2026-10-05 ### Added - `app.ask(question)`: a typed question to the app's agent, answered from the business's content with sources, as the signed-in person (their customer groups apply). Returns `{ status, answer, sources, questionNotice }`; show `questionNotice` with a person's first answer. See [Ask by text](https://hub.awesomate.ai/docs/sdk/guides/ask/). - Type `AgentAnswer`. ## [0.19.0] - 2026-10-05 ### Added - `db.tasks`: the account's Tasks board from your server or an n8n workflow: `board()`, `give()` a task to someone on the account, `update()`, `done()`, `drop()`, and `notNow()` / `passOn()` for any card. Account token only. See [Tasks](https://hub.awesomate.ai/docs/sdk/guides/tasks/). - Types `TaskBoard`, `TaskCard`, `TaskPerson` and `GiveTask`. ## [0.18.1] - 2026-10-05 ### Fixed - `app.here()` on a record another part of the page already has open (a thread and a sidebar on the same job): the first one stopped hearing who was there and its `typing()` did nothing, and when the second called `leave()` the hub was told this person had left while the first was still on the record. Now every part hears `onPeople`, a part that opens the record later hears who was last there, typing shares one clock per record, and the record closes only when the last part leaves. It counts once towards the 5 records a connection can have open. ## [0.18.0] - 2026-10-05 ### Changed - `db.files.makePrivate()`: a file made private, deleted or replaced now stops being served from its public address within seconds, because the hub clears Cloudflare's copy. Before, the old copy could keep loading for up to 4 hours. A browser that already opened the file may still keep its own copy for a while. ## [0.17.0] - 2026-10-05 ### Added - `createBookingsClient({ key })`: a business's own booking page for visitors who are not signed in. `services()` and `openTimes()` list what can be booked and when, `book()` books (the customer gets an email with an invite and a link to change or cancel), and `booking()`, `openTimesToMove()`, `move()` and `cancel()` work from that link. Uses the booking key (`bk_`) from Contacts, Bookings, so it works on every plan, from the sites the key lists. See [Bookings](https://hub.awesomate.ai/docs/sdk/guides/bookings/). - `manageTokenFrom(url)`: the token in a booking's manage link. ## [0.16.0] - 2026-10-05 ### Added - `db.files`: the account's Files (the folders on its automation account) from your server: `list`, `search`, `usage`, `upload`, `download`, `mkdir`, `move`, `makePublic`, `makePrivate`, `remove`. Files in `public/` come back with a `public_url`. Needs the account's token with `files:read` or `files:write`; an app's server key is refused. See [Files and uploads](https://hub.awesomate.ai/docs/sdk/guides/files/). - `app.files`: people signed in to your app can attach files when the account switches uploads on for the app: `mode()`, `upload()` (up to 10 MB, answers a handle to keep in a record) and `download(handle)`. - Types `FileEntry`, `FilesUsage`, `AppFile` and `FileBody`. ### Changed - A 411, 413 or 415 from the hub now reads as `validation`, and `consent_denied` as `consent_blocked`. ## [0.15.0] - 2026-10-05 ### Added - `db.businessMap()` now returns `path`: how a customer moves through the business, step by step, with the division, the job that looks after each step, who that is, and the hand-off. When the owner has not set one, it is the path suggested for their kind of business (`path.stored` is false). - Each job's `proceduresTag` (such as `job:quotes`), the tag its procedures carry in 1Brain, and each division's `oneBrainDepartments`. - Gap kinds `procedure_first` (a job something helps with, but nothing says what it covers), `path_missing` and `path_unowned`. ## [0.14.0] - 2026-10-05 ### Added - `db.businessMap()`: the business map, read only. The seven divisions, the jobs in each and who holds them, which agents and automations help which job and how far each may go, and what is missing, most important first. Account token only. See [Your business map](https://hub.awesomate.ai/docs/sdk/guides/business-details/#your-business-map). ## [0.13.0] - 2026-10-04 ### Added - `app.voiceAgent()` and `app.voiceSession()`: let the people signed in to your app talk by voice to the agent the app names, with the hub's `AwesomateAgent` widget. See [Talk by voice](https://hub.awesomate.ai/docs/sdk/guides/voice/). - `AwesomateError.personMessage`: the sentence to show the person using your app, when the hub sends one (a refused voice call, for example). - The query filter types `TextOps`, `NumberOps`, `BoolOps` and `ListOps` are exported. - Documentation at https://hub.awesomate.ai/docs/sdk/, with a reference generated from this package's code, and `llms.txt` for AI tools. ### Fixed - A filter on a choice column can name several of its choices: `{ status: { in: ['open', 'quoted'] } }` was a type error with generated types, because the filter type was worked out once per choice. Found by the new docs build, which type-checks every example. - `VERSION` reports this package's real version. It had said `0.7.0` since version 0.8.0, and the server client sends it with every request. The docs build now fails if they differ. - The voice methods were merged in the hub on 2026-10-04 but missed npm: 0.12.0 had already been published from another change. They ship here. ## [0.12.0] - 2026-10-04 ### Added - `db.business()` and `db.businessSuggestions()`: read what Awesomate knows about the business (name, what it does, voice, colours, how customers reach it), with where each detail came from. Account token only. ## [0.11.0] - 2026-10-04 ### Added - `app.here(recordId, onPeople, onError?)`: who else has a record open, who is typing, and the app's assistant while it writes a reply. Returns `typing()` and `leave()`. See [Who's here and typing](https://hub.awesomate.ai/docs/sdk/guides/who-is-here/). ## [0.10.0] - 2026-10-04 ### Changed - A live connection in a browser tells the hub whether the tab is in front. Reply emails now wait only while someone is actually looking at the app; a tab left open in the background no longer holds them back. ## [0.9.0] - 2026-10-04 ### Added - `app.call(recipe, args)`: a signed-in person runs a write recipe the account opened to their role, such as accepting their own quote. See [Let customers take an action](https://hub.awesomate.ai/docs/sdk/guides/customer-actions/). ## [0.8.0] - 2026-10-03 ### Added - `app.lookupCustomers({ q | ids, limit })`: staff in the roles an app allows find a customer and put a record under them. ## [0.7.0] - 2026-10-03 ### Added - `createClient` accepts an app server key (`ak_...`) as well as the account's token. A key reads and writes records of its own kinds and runs saved queries and recipes; it cannot change the account's shape. ## [0.6.0] - 2026-10-03 ### Added - `auth.onSignInError(listener)`: hear when a sign-in link has expired or was already used. - The `handleSignInLinks` option (on by default in a browser). ### Changed - A sign-in link signs the person in by itself, on page load or when it arrives in a tab that is already open. Calling `auth.completeSignIn()` as well is safe, and returns the same sign-in. ## [0.5.0] - 2026-10-03 ### Added - `app.live(kind, options, onRows)`: a list kept current over one WebSocket, re-run as the person on every change. See [Lists that keep themselves current](https://hub.awesomate.ai/docs/sdk/guides/live-lists/). ## [0.4.0] - 2026-10-03 ### Added - `createAppClient({ publishableKey })`: the app client. Your app's people sign in by email link (`auth.signInWithLink`, `auth.completeSignIn`, `auth.user`, `auth.onChange`, `auth.signOut`, `auth.accessToken`) and then `query`, `get`, `write` and `archive` as themselves, inside the account's rules. - `signInTokenFrom(url)`. ## [0.3.0] - 2026-10-03 ### Added - Saved queries: `db.queries()`, `db.saveQuery()`, `db.run()`, `db.archiveQuery()`. - Write recipes: `db.recipes()`, `db.saveRecipe()`, `db.call()`, `db.archiveRecipe()`. - Generated types cover saved queries' parameters and rows, and recipes' arguments. ## [0.2.0] - 2026-10-03 ### Added - App kinds: `db.kinds()`, `db.defineKind()`, `db.write()`, `db.archive()`. ## [0.1.0] - 2026-10-03 ### Added - `createClient({ token })`: read your contact list with `query`, `queryAll`, `get`, `aggregate` and `schema`. - `npx @awesomate/sdk types`: TypeScript types generated from your own account's fields.