# Avi — The AI assistant for real work. Exceptional at the everyday work of a company — email, meetings, deals, documents, and support. Avi works on screen where you can watch it, distills everything into a feed of what matters, and turns repeat work into automations that run on their own. - SOC 2 Type II Audited Team - Google CASA AL2 Certified - Zero Data Retention by AI Models Avi is invitation only, for now. [Continue with Google](https://cloud.avi.run/api/v1/auth/google?redirect_uri=https%3A%2F%2Fapp.avi.run) · [Build on Avi](https://avi.run/docs) ## The most powerful and visual apps in AI. Not connectors — full apps. Email, Calendar, Drive, CRM, Messages, Code and more, on your Google, Slack, HubSpot, and GitHub accounts — each with beautiful live panels, deep tools, its own data, automations, and voice controls. One click to connect, instantly usable by you and your AI. Connects with: Google, AWS, HubSpot, GitHub, Slack, Linear, X, Freshdesk, Resend. ## The AI-curated feed. Your whole world, minus the noise. Avi and your apps produce a constant stream of events. Avi's curation engine reads every one and distills them into relevant, high-signal updates. ## Automations on your payroll. Delegate the recurring work: automations run on schedules or react to events — triaging support, chasing renewals, watching costs — and report every outcome back to your feed. ## Build your own rich apps. Everything that makes an Avi app powerful — live panels, deep tools, its own data and automations — is yours to build with. Serverless hosting, data/file storage, and vector search come standard. Bring your workflows, ship in one deploy. ## White-glove setup. Live in days. One flat fee. Our expert team connects your apps, brings in your data, and stands up your first agents and automations — with a partner on call from day one. [Chat with us](https://meetings.avi.tools/3vpgQXK4_R1e) · [Continue with Google](https://cloud.avi.run/api/v1/auth/google?redirect_uri=https%3A%2F%2Fapp.avi.run) ## More about Avi - [Pricing](https://avi.run/pricing.md) — $79 per person per month billed annually, or $99 monthly. - [Documentation](https://avi.run/docs.md) — guides for every part of Avi. - [Public API](https://avi.run/docs/api.md) — Avi's docs, pricing and trust details as JSON, described by OpenAPI. - [Trust](https://avi.run/trust.md) — certifications, AI data retention, encryption, and subprocessors. - [About](https://avi.run/about.md) · [Contact](https://avi.run/contact.md) · [llms.txt](https://avi.run/llms.txt) --- # Getting Started Avi is the AI assistant for real work. It works inside projects you organize around whatever you're doing — a company, a side project, a research area. You chat with it, and it can run tools to read and change real things: files, notes, GitHub issues, and more. It can also set up automations that keep working on a schedule after the chat ends. ## 1. Sign up Head to [avi.run](https://avi.run) and sign in with Google. Avi is currently **invite-only**, so a new sign-in lands on the waitlist. There are two ways in: - **Enter an invite code** on the waitlist screen. - **Accept an invitation** from someone already using Avi. Invitations sent to your email address show up right on the waitlist screen, and the link in the invite email works too. Sign in with the address the invitation was sent to. Once you're in, Avi creates your **personal account**, a **personal organization**, and a private **Personal project** you can use right away. You start with 1,500 free credits — no card required — and connecting your first account (Google) adds **1,500 more**; see [Billing](/docs/billing). ## 2. Pick your apps — Avi sets itself up The first time you open your Personal project, Avi asks what it should take care of — Email, Calendar, Drive, Contacts, Messages, and more, each a card with its own toggle. Pick the ones you want and sign in once per service; the moment a connection completes, those apps configure themselves: Email shows your Gmail, Calendar syncs your calendars, Drive brings in your files. No per-app setup. Connecting also adds the **1,500 free credits** mentioned above. Your data stays yours: neither Avi nor its AI model providers train AI models on it, it's processed only to run Avi for you, and our team doesn't read it except with your permission, to investigate a security issue, or when the law requires. You can disconnect any account from the settings of the app that uses it, and you can skip the whole step — every app works on its own, and each one offers to connect its service right where you'd need it. ## 3. Open your first project After sign-in you land in the web app at [app.avi.run](https://app.avi.run). Your Personal project is already created. Type a message in the chat to start working with the agent. Simple ways to get oriented: - "What can you do?" - "Take notes on the following plan: ..." - "Email me the weather every morning." ## 4. Invite teammates (optional) If you want other people in the same workspace, create a new **org** and invite teammates. Each member gets their own private Personal project inside the org, and anyone in it can make agents the org owns. See [Organizations & Projects](/docs/orgs-and-projects). ## 5. Optional: add Avi to your phone's home screen On your phone, open [app.avi.run](https://app.avi.run) and add it to your home screen (on iPhone: Share → **Add to Home Screen**; on Android: browser menu → **Add to Home screen** / **Install**). You get the Avi icon on your home screen, and it opens full screen like a native app. ## Key ideas - **Org** — a workspace you own or belong to. Your personal org is created automatically. - **Home feed** — what you see when nothing else is open: one row per topic (a deal, a feature, an incident, one of your tasks), newest activity first, each showing its latest development. Click a row to open its page — anything waiting on you at the top, then its whole history; **back** returns you to the feed. - **Project** — where all of Avi's work lives: chat history, tasks, notes, files, instructions. - **Agent** — a project your org owns, with its own name, picture, handle and people; its automations are its work. See [Agents](/docs/agents). - **Tool** — something the agent can do for you (send an email, search the web, read a file). You control which tools are on in each project. - **Task** — a to-do item you and the agent track inside a project. - **Automation** — work Avi runs in the background: once, on a schedule, or whenever something happens, reporting its progress as it goes. - **Wiki** — your org's shared knowledge: company facts, skills (step-by-step playbooks the agent follows) and a page for each person. - **App** — a bundle of related tools (e.g. the Code app, the CRM app). Turn apps on per project. Ready to go deeper? Start with [Organizations & Projects](/docs/orgs-and-projects). --- # Organizations & Projects Avi is organized as **accounts** → **orgs** → **projects**. You don't need to think about most of this day-to-day, but knowing the pieces helps when you collaborate or want to keep work separate. ## Accounts An **account** is the billing unit. When you sign up, you get a personal account automatically. Your trial credits and, later, your subscription live on the account. See [Billing](/docs/billing). ## Organizations (orgs) An **org** is a workspace. Every account has one **Personal org** created at signup. You can also create or join **shared orgs** to collaborate. - **Personal org** — always yours and can't be deleted. It gets a random name at signup (something like *Amber Falcon 314*); rename it any time from its settings. Your personal projects live here by default. - **Shared orgs** — created by you or someone who invited you. Orgs have members with roles: - **Owner** — full control, including billing and member management. - **Admin** — can manage members and change settings. Can't manage billing. - **Member** — has their own private Personal project in the org and can make agents the org owns. Each member has one private **Personal** project — nobody else in the org, owners and admins included, can see it — and can make **agents**, projects the org owns, that the people on them share. See [Agents](/docs/agents). ## Projects A **project** is the container for a body of work: chat history, tasks, notes, files, instructions. Every project has its own settings — its own tools enabled, its own agent instructions, its own conversation. There are two kinds: your own **Personal** project in each org, and **agents** the org owns. ### Personal projects Every user in an org has a private project called **Personal**. Only you can see it, read from it, or chat in it — not even org owners or admins can. It's the right place for personal work inside a shared org (e.g. your own to-dos, your own notes) without worrying about other people seeing it. ### Agents The other kind of project is an **agent**: a project the org owns, with a name, a picture, a handle and people at two roles — members, who see it and talk to it, and managers, who change it. Its automations are its work, and the accounts it connects are the team's. Make one from the org menu (**New agent…**). See [Agents](/docs/agents). ### Per-project agent settings Each project can have: - **Instructions** — persistent guidance the agent follows in this project. Good for domain-specific rules, style preferences, or constraints. ("Always respond in English." "This is a Node.js project using ESM." "Never touch the `prod` branch.") - **Enabled apps** — which bundles of tools the agent has access to. See [Apps](/docs/using-apps). - **Tool permissions** — individual tools can be turned off even if their app is enabled, for finer control. - **AI model** — override the org-level default. See [AI Models](/docs/models). ## Inviting teammates From an org's settings, invite people by email. They receive a link that expires in 7 days. Accepting joins them to the org. Each new org member gets their own private Personal project inside that org automatically. --- # Chatting with Your Agent Each project has one conversation with the agent. Open a project and start typing — that's it. ## How it works - Each message you send can trigger the agent to **call tools** — searching the web, reading files, writing notes, creating tasks, or whatever else you've enabled in the project. - When the agent **searches**, one search covers your project's own data at once — updates, files, and everything your apps keep here — and reaches into a connected app's own search (your CRM, mailbox, chat workspace, cloud drive) when your question points there or when you ask it to search everything. It can look for several things in a single search, and every result says which app it came from. - The agent can chain multiple tool calls together to complete a request. You'll see each step as it happens. - Conversations are **persistent** — the agent remembers what you said and did in the project, across sessions. When a conversation gets long it's automatically summarized so older context stays available without slowing things down. - Responses are **resilient** — if you close the tab, put your laptop to sleep, or lose connection mid-response, the agent keeps working. Reopen the chat and you'll rejoin the live response (or find it finished and waiting). Only the Stop button cancels a run. - Links the agent shares on their own line show up as **link cards** — a clickable box with the destination's icon and a readable title. Cards into Avi (an app panel, a settings page) open right in place; external links open in a new tab. A card for a file or a folder always shows that file's or folder's own name. ## The agent sees what you're looking at When you chat with a panel open beside the conversation, the agent knows which panel is focused and what it's showing: an open email or note and how it begins, or which list you're on. Ask about it directly ("what's this note about?", "reply to this") without pasting the content in — typed or spoken. In a voice session the agent also sees the rows and buttons on screen, so it can act on them ("open the third one", "close this"). A few things are handled on the spot without a round trip: navigation ("open my contacts", "go back", "scroll down"), and the view zoom — "zoom in", "zoom out", "bigger", "smaller", "zoom to 150 percent", "reset zoom" — which scales the whole window in 10% steps between 50% and 200% and is remembered per browser. ## Getting good results - **Be concrete.** "Summarize today's GitHub notifications and post an update" beats "check GitHub." - **Add project instructions** for things that apply every time (see [Projects](/docs/orgs-and-projects#per-project-agent-settings)). You only have to say them once. - **Write a skill in the wiki** when you have a procedure the agent should follow — a multi-step workflow, a style guide, a specific prompt template. See [Wiki](/docs/wiki). - **Pick the right model** for what you're doing. More capable models use more credits but do better on harder reasoning. See [AI Models](/docs/models). ## Queueing messages You don't have to wait for the agent to finish before sending your next message. If you send while it's still responding, the message is **queued** — it shows up below the response with a "Queued" label and sends automatically, in order, as soon as the agent finishes. - Queue as many messages as you like; they send one at a time. - Changed your mind? Click the **X** next to a queued message to cancel it before it sends. - Queued messages live in your browser tab — closing the tab discards any that haven't sent yet. ## Bigger work runs in the background Avi does most work right in the conversation, and that includes Word documents and Excel workbooks: drafting a quote from your template, changing wording or a table, redlining a contract, building a budget, adding rows and formulas to a spreadsheet. It brings its Word or Excel know-how to the request and answers in the same reply. Work that would keep you waiting runs in the background instead, as a [one-time automation](/docs/background-agents#one-time-automations): - **Research** takes a question that needs real looking. - **Anything long** that you ask for once, such as going through a folder of files or comparing several vendors. Avi says so in one line, and the conversation stays free for your next message. A card appears in the chat and shows each step while the work runs. When it is done, the answer appears at the end of that message: the short version first, with **Show more** for the specifics, and a tile for each file it created or changed. In voice mode you hear the short version, and a file it made opens on screen. It is in your Automations list while it works, and in History once it is done; its result is also a line on the project's single **One-time automations** row in Updates, so you can find it again later. Two kinds of request wait on other people for days, so Avi sets up an [automation](/docs/background-agents#meetings) for each one: - **Meetings** books meetings with other people. - **Requests** collects things people owe you. Their card in the chat keeps itself current, and they report to [Updates](/docs/updates) as they go. Ask about any result whenever you like and Avi picks it up from there. Avi creates an automation of your own only when you ask for one or the work is ongoing, such as every morning or whenever something happens. ## Seeing what Avi found When seeing beats reading — product photos, pages worth opening, a side-by-side comparison, where each of several things stands — Avi shows it: the things it found, each with its picture, name and a line about it, plus a comparison table and a short note, together. In a written chat it appears right in the reply; in a voice session it shows on your home screen instead (see below). Things that live in an app — an email, a file, a deal — are shown by opening them in that app instead. ## Stopping an in-flight response If the agent starts doing something you didn't want, use the Stop button in the chat. It interrupts the current run immediately. If you have messages queued, the next one sends once the run stops — cancel them first if you don't want that. In voice mode, if Avi starts on your request while you are still talking, just keep going: Avi works out that the rest belongs to the same request. If it hasn't done anything yet it starts over with the whole thing; if it has already made something, it applies what you added as a change to that work rather than doing it twice. Something unrelated runs as its own request. Say "repeat that" or "say that again" to hear the last thing Avi said once more. Background noise that contains no words is ignored, and so is a stray "um" or "okay" on its own. In voice mode, say "stop", "don't", or "wait" to stop everything — the talking and every request in progress. To only make Avi stop talking, say "stop talking", "be quiet" or "don't talk": the reply is cut, but every request keeps running and you hear its answer when it's done. Screen commands and new requests work while Avi is talking, without interrupting it. Say "mute" to mute the microphone without leaving voice mode — it works while Avi is working or talking too, and whatever Avi is doing carries on — and "unmute" to bring it back. While muted, Avi ignores everything it hears except "unmute". The voice options menu under the orb has the same Mute / Unmute item, and a **Microphone** entry that opens a list of your inputs (available whether or not voice mode is on): "System default" follows your computer's sound settings, or pick a specific microphone to use in this browser; during a session the list also shows which microphone Avi is listening to right now. If the microphone in use is unplugged, Avi switches to what's left and says so. You can also change the AI model by voice: "switch to Sonnet", "use the Haiku model", "change the model to Opus five" — you don't need to pronounce the name perfectly, and if two models share a name Avi asks which one. In voice mode, every request you speak runs in the background: a ring around the orb shows it's registered, a soft pop plays when you add a request while another is still running, a small circle on the orb shows how many requests are still owed to you — running, or finished and waiting to be read — and Avi is ready for your next one straight away — you can keep talking, even while it's reading you an answer. Whatever it creates or edits for you — a draft, an event, a note, a file — opens on screen the moment it's written. When a request is done, Avi says its answer at the next quiet moment; if the request was to write or change something and it went as asked, it says nothing at all — the app's own chime and the change on screen are the confirmation — unless you also asked it something in the same breath, in which case it reads you the answer. Ask if you want the details. Independent requests run at the same time, and each answers when it is done; a request that builds on the one before it waits for that one to finish. "Stop" stops all of them — the one running, any waiting behind it, and whatever Avi is saying — and so do "stop all jobs", "stop everything", "cancel all requests", and "clear the queue". These are handled by the app on the spot and never reach the assistant; when more than one request was stopped, Avi says "Stopped everything." Say "use the default model" to go back to your org's default. While Avi is working on a spoken request or reading its reply, only screen commands are taken ("open email", "scroll down", "go back"); anything else you say — or any background noise it hears — is ignored without comment, and nothing is queued behind the current request. Voice stays in the project you're in; switch projects from the project menu. ## When a message doesn't go through Sometimes a turn fails outright — the model provider is overloaded, the conversation has outgrown the model's context, or something you attached can't be read. When that happens the chat tells you what went wrong in place, with the underlying error underneath it, instead of leaving a blank reply. Most of these clear on a retry; a conversation that's grown too long is the one to clear. ## Sounds Avi plays a short sound when a reply finishes while you're away from the window, when something fails, when a new update lands in your feed, and when voice mode starts or stops. In voice mode, where your eyes may be elsewhere, it also sounds when something succeeds (an email sent, a draft created), when a confirmation needs you, and when the record you're looking at changes underneath you; in written mode those moments are already on screen, so they stay silent. Turn them off or on for the browser you're in under **Settings → General → Sounds**. The choice is per browser, so your phone and your laptop can differ. You can also ask for "some music" in the conversation, spoken or typed, and quiet background music starts and keeps playing whether you are talking or typing; it fades under every voice. "Pause the music", "stop the music", "next track", "skip this song", "quieter", "louder", "set the music to 20 percent", and "what's playing" control it, and it stops when you leave the voice session. "Next track" and "skip the song" only mean the music while it is on; otherwise they reach Avi like anything else you say. Anything Avi understands about the music it can do for you as well, so "put on something calm" or "make the music quieter" in writing works too. Each "louder" or "quieter" is a clear step, and 5 percent is close to silent. Hover the music note beside the orb to see what is playing, pause or stop it, and set the volume. When the music comes from a licensed station, the station decides whether a song can be skipped and there is no going back to the previous one; Avi tells you when it can't. Avi knows when music is on and what's playing, so you can ask about it in the conversation too. When you ask Avi to show you something in a voice session, what it found takes the place of your updates on the home screen, laid out the same way it is in the conversation, with each page's own picture and icon where the page provides them. Press the back arrow, or say "back" or "close that", and your updates return. ## Clearing history You can clear a conversation's history from the project's chat menu. Useful if you want to start fresh or if the agent's gone off on a tangent. Clearing takes effect everywhere at once — the conversation history lives on the server, so any other tab, device, or voice session on the same conversation starts from the cleared state on its next message. Nothing carries the old history forward. ## Task conversations Tasks don't run on their own, and they don't have their own chats — talk about a task in the project chat (`+`-mention it to point the agent straight at it). For work that should happen on a schedule, the agent sets up an [automation](/docs/background-agents). See [Tasks](/docs/tasks). --- # Tasks Tasks are to-do items — things you or the agent want to track inside a project. Use them for anything with a "done" state: work to get to later, steps in a bigger effort, or items the agent creates while working for you. Tasks are a first-party **app** that is always on in every project — there's nothing to enable, and its panel sits on the rail like any other. Tasks don't run on their own. If you want Avi to *do* something on a schedule or in the background, ask it in chat and it will set up an [automation](/docs/background-agents) — tasks are for tracking, automations are for running. ## Creating a task Open the **Tasks** panel from the rail and click **New task**. The new task opens beside the list, where tasks open. Fill in its name, status, priority and labels, then press **Create**. You can also just ask the agent ("add a task to review the Q3 numbers") — it creates and updates tasks with the same fields you see. ## Fields - **Name** — what the task is. - **Notes** — free-form markdown: persistent context about the task (links, requirements, decisions). Yours to own — nothing writes them automatically. - **Priority** — every task has one, from 1 to 4, and higher means more important: **4 Urgent**, **3 High**, **2 Medium**, **1 Low**. When the agent creates a task it judges the priority from what you said ("an urgent task to…", "when you get a chance…"); with nothing to go on, a task starts at 1. - **Status** — **Incomplete**, **In Progress**, **Complete**, or **Cancelled**. Change it from the task's row in the list, or on the task's page. - **Labels** — any of the project's [labels](#labels), such as "Errands" or "Finance". Add and remove them on the task's page. The list has three columns: **Task**, **Status** and **Priority**. A task's labels show in small type under its name. You can change a task's status or priority right from its row, without opening it. Priority shows as four signal bars: **Urgent** lights all four in red, **High** three in orange, **Medium** two in yellow, and **Low** one in grey. The list shows your unfinished work, meaning Incomplete and In Progress tasks, with the most important first and the newest first within each level. Click the **Task**, **Status** or **Priority** header to sort by it, and click again to reverse the order. Sorted by status, each status still lists the most important first. You can also say "sort by status" or "sort by name". Use the **Filter** menu to show any mix of the four statuses, or only tasks with certain labels (a task shows if it has any of the labels you pick); **Clear filters** shows every task. On a task's page, status, priority and labels save the moment you change them. The name and notes save when you press **Save**. Tasks used to have due dates. Tasks created before priorities got a priority from their due date: overdue became Urgent, due within a week High, due within a month Medium, and everything else Low. The old date stays on the task for reference. Every task also carries a `metadata` bag your apps can write to — see [Data Types](/docs/data-types). ## Subtasks A task can have subtasks — useful for breaking a larger piece of work into steps, or for letting the agent lay out a checklist beneath a parent task. Click a task's **Subtasks** tab to see its subtasks as a list of their own, which works like the main list. It has the same search box, the same filters (Status and Labels), sorting by column, and the same Status and Priority controls on each row. The path at the top reads Tasks / task / Subtasks. Click the task's name there, or the back arrow, to return to the task. **New subtask** opens a new subtask in the same pane. When you open a subtask, the list beside it shows its parent's subtasks, with the same search and filters, and the path reads Tasks / task / Subtasks / subtask. Closing a subtask (the **×**, or saying "close") takes you back to that subtasks list; click **Tasks** in the path for the full task list. A subtask's page shows **Subtask of** and its parent's name, and the path at the top reads Tasks / parent / subtask. Click the parent to go back to it. Rules worth knowing: - **Subtasks go one level deep.** A subtask can't have subtasks of its own, and a task that has subtasks can't be moved under another task. - **Completing a parent never completes its children.** They're separate to-dos. - **Deleting a task deletes its subtasks with it.** ## Labels Labels are your own way to group tasks, such as **Errands**, **Finance** or **Waiting on someone**. A task can have any number of them. To manage them, click the **⋮** options button next to **New task** and choose **Manage labels**. The labels page lists every label with its description. It works like the task list: click a label and it opens beside the list. From there you can: - **Create a label** with **New label**. Give it a name, a description and a color, then press **Create**. - **Edit a label**: change its name, description or color, then press **Save**. - **Delete a label** with the trash button at the bottom of an open label. The label comes off every task that had it, and the tasks themselves stay. **Write a description.** Avi reads it to decide when a label fits. For example, "anything I have to leave the house for" makes Errands useful to the agent. **Avi labels new tasks automatically.** When Avi adds a task for you, whether in chat, from an automation or when you say "add a subtask …", it applies the labels whose descriptions fit, unless you told it which labels to use. It uses only the labels you have made and doesn't invent new ones unless you ask. If a label doesn't fit, take it off on the task's page or say "remove the errands label". Tasks you create with **New task** keep exactly the labels you picked. Each label has a color: blue, green, orange, purple, red, yellow or grey. These are the same hues the priorities and badges use. A new label takes the next color that hasn't been used much yet, and you can change it any time. On a task's page, the **Labels** field shows the task's labels. Click **Add label** to attach one, and click the **×** next to a label to take it off. If something can't be saved, the reason shows at the top of the task or label you have open, and it clears itself after a few seconds. ## Assigning and sharing Your tasks are yours: nobody else in the org sees them unless you hand them over. There are two ways to do that. - **Assign a task** to one or more people in your org. They become responsible for it: it shows up in *their* Tasks panel, they can start, complete and edit it, and they're told about it. On a task's page, use **Assign** under **People** to pick people, or ask Avi: "assign the proposal to Matt", "give the tax return to Sam". You can assign a task to yourself too, and a new task can be assigned as you create it. - **Share a task** with people, or with everyone in the org, without making anyone responsible. Under **People** on the task's page, use **Share** to pick a person (they can view it), or turn on the **Everyone in the org** toggle next to it (everyone can view it). It's off unless you turn it on. Ask Avi the same way: "share the launch checklist with Sam", "share it with the whole org". A task's **People** section lists everyone who has it, once each, with what they are: **Owner**, **Assigned** (responsible for it, so they can edit it), **Can view** or **Can edit** (shared, not responsible). On your own task you manage it there. On someone else's task the list is read-only, and one line at the top says whose task it is and what you are on it, such as "Austen Collins's task · assigned to you". A task that someone assigned or shared to you appears in your list alongside your own, with the owner's name under it. If you can edit it, changes you make are made to *their* task, so you both see the same thing. If it was shared with you to view, its page is read-only. Only the task's owner can delete it, move it under another task, label it, or add subtasks under it. Labels are each person's own vocabulary, so a task's labels show only in its owner's list. To change what someone you shared with can do, pick **Can view** or **Can edit** next to their name, or say "set Sam's access to edit". Sharing covers the task's subtasks too, at the same level, and a subtask the owner adds later is shared the same way. A subtask shows who it's shared with but has no sharing controls of its own: it says "Sharing follows the parent task", with a link to the parent task, where you change it. A subtask can still be assigned to someone on its own. Taking someone off a task (unassigning them) takes their access with it. To let them keep seeing it, share it with them instead. Someone assigned to a task can already edit it, so to make them view-only, unassign them first and then share it with them. Use the **People** filter to show only tasks **assigned to me**, **shared with me**, or **my own**. Every task's row shows who it's assigned to, by name. ## Associated records A task can have associated records — the things it's about: a CRM deal, contact or company, an email thread, a message, a Drive file, a calendar event, or another task. Ask Avi, for example "associate this task with the Acme deal", or "make a task to follow up on this email" while you have the email open. A task holds up to 10 associated records. They show as small tags with each record's app icon. In the task list and a task's subtasks list, a task shows its first two and "+N" for the rest. On a task's **Details** tab, every one is listed under **Associated records**, below **People**. Click one to open the record in its own app. To take one off, click the **×** next to it (you need to be able to edit the task). Associating a record with a task never gives anyone access to that record. People who can see the task see its title, but opening the record still needs access to it. ## Finding tasks The panel has a search box at the top: type to filter to tasks whose **name or notes** contain what you typed. Tasks also turn up in Avi's global search, and you can `+`-mention one in chat to point the agent at it. ## Saying it out loud In voice mode the Tasks panel listens for the same things you would click: - **Find a task** — "open the third task", "open the invoices one", "search for tax", "search tasks for invoices", "clear the search". - **Update any listed task** — "mark groceries as complete", "change the tax return to priority 3", "set invoices to done", "move the offsite to in progress", "update the lease to urgent", "make taxes high priority". You don't have to say punctuation, and near forms count ("follow up" for "Follow-up", "setup" for "Set up"); one misheard word in a longer name is forgiven. Anything that isn't a status or priority change for a task on screen — "update the notes of this task with the latest…" — goes to Avi as a normal request. Say enough of the name to pick one task; if two tasks fit, Avi asks for more of the name instead of guessing. - **Label tasks** — "tag groceries errands", "tag the tax return as finance", "add the errands label to groceries", "put the finance tag on the invoices", "remove the errands label from groceries", "untag the tax return finance". With a task open: "tag it errands", "add the finance label", "remove the errands label". To use "add", "put" or "remove", say the word "label" or "tag". "Tag" and "untag" work with the name alone. - **Manage labels** — "manage labels" or "show labels" opens the labels page. You can also say "new label receipts" or "create a label called Avi with the description our product" from anywhere in the panel. If the name can't be saved as said, for example because it's longer than 40 characters, the new label opens filled in so you can fix it. On the labels page: "make errands red", "change the finance label to purple", "open the errands label", "delete the receipts label" (Avi asks you to confirm), "back to tasks". - **Assign tasks** — "assign groceries to Matt", "give the tax return to Sam Lee", "delegate the lease to Ryan", "assign the third task to me", "unassign Matt from groceries". "Reassign the tax return to Sam" hands it to Sam and takes everyone else off. With a task open: "assign to Matt", "assign it to me", "reassign this to Sam". A first or last name works when only one person has it; if two people fit, Avi asks which. - **Share tasks** — "share groceries with Sam", "invite Matt to the tax return", "share the tax return with everyone" (or "with the team"), "unshare groceries from Sam", "stop sharing it with everyone". With a task open: "share with Matt", "share this with everyone", or just "everyone can see this". A share lets them view it, the same as picking them under **People**. Only the task's owner can share it. - **Narrow the list** — "only complete", "only in progress", "filter by cancelled", "only incomplete", "filter status to complete and cancelled", "filter by errands", "only assigned to me", "filter by shared with me", "only my own", "clear the filters". Filter words narrow the list; they never change a task. - **With a task open** — "set the status to in progress", "change status to cancelled", "start it", "mark it in progress", "mark it done", "reopen it", "cancel it"; "set the priority to high", "set priority to 4", "make it urgent", "low priority", "bump it up", "lower the priority". "Change the status" or "change the priority" opens the picker so you can choose. - **Edit and add** — "rename the task to …", "new subtask" or "add a subtask" (opens one to fill in; to name it in one go, ask Avi: "add a subtask to buy milk"), "new task", "type in the name …" then "save" or "create it", and "delete the task" (Avi asks you to confirm). - **Get around a task** — "show subtasks", "open subtasks" or "list the subtasks" opens its subtasks; "details" goes back to the task. On a subtask, "go to the parent" or "parent task" opens the task it belongs to. "edit notes" opens the Notes tab. A voice command runs on its own only when the whole sentence is the command; anything longer, or a question, goes to Avi. In voice mode, a new task plays the success chime — one you create, a subtask you add, or one Avi creates for you when it shows up in the Tasks panel. ## In the feed Your tasks are topics in your [Updates](/docs/updates) feed. A task never appears there on its own. It shows up when something happens that is the task's work: an email from the vendor it names, the document it was waiting on, someone asking how it's going. That activity lands on the task itself, titled by the task's name, instead of on a separate topic. If a topic about the same work already exists, it is folded into the task, so you see one row named for the task. Later activity about it adds to the same topic. Completing or reopening the task shows there too, and renaming the task renames the topic. Open the task from the topic with **Open task**. ## Want it to happen automatically? Tell the agent in chat — "check the homepage every 15 minutes and page me if it's down", "every Monday morning summarize last week's GitHub issues". It will set up an [automation](/docs/background-agents) that runs on that schedule and reports progress to the Automations panel. --- # Contacts Contacts is the project's address book: the people this project deals with, with their names, email addresses, phone numbers, company, title, tags, and notes. It's a first-party **app** that is always on in every project — there's nothing to enable, and its panel sits on the rail like any other. It works with nothing connected, and it syncs with Google Contacts when you point it there. This is the *project's* address book. The **CRM** app's people are a different thing — your org's shared book of business, with companies, pipelines, and deals — and the two are deliberately separate. ## Where your contacts come from Contacts has a **Source** setting with two values: - **Standalone** (the default) — Avi's own contact list. Nothing to connect. - **Google Contacts** — the Google account you bind in the same settings. The source is a **mode, not a blend**: only the active source's contacts appear in the panel, in the agent's tools, and in search. An address book that silently mixes two books is one you can't trust. Switching sources **hides, never deletes**. The other book stays exactly where it is and comes back the moment you switch back. Disconnecting a Google account doesn't change your data either — only ongoing sync stops. Change the source in **Project Settings → Apps → Contacts**. The panel's Settings tab shows what's active and when Google was last checked. ### Syncing with Google The first pass loads your whole Google contact list; after that a pass every ten minutes pulls just what changed. While Google is the active source, creating or editing a contact writes to Google first and only then mirrors locally — if Google refuses, the write fails and tells you why, instead of quietly leaving you with two different truths. Two things stay local: **notes** and **tags**. Google never sees them. Google's auto-saved "other contacts" — the addresses it keeps every time you mail someone new — are never synced. Only your real contact list comes across. ## Notes are yours A contact's **notes** are free-form prose that a person writes. Avi does not maintain them: no enrichment, no agent, no sync pass, and no background job writes them. **What changed:** Avi used to keep an AI-written "context" block on every contact and refresh it in the background. That's gone. When the Contacts app shipped, each contact's existing context was appended to that contact's notes exactly once, under a dated marker — so nothing was lost, and nothing has been written there since. Contacts are also no longer created behind your back. Apps that read your email, Slack, or X used to mint a contact for every new person they saw; they don't any more. Contacts are the people you (or the agent, when you ask) put in the book. ## Finding people The panel's search box searches **name, email, company, and title**. An email address also matches a person's secondary addresses, so looking someone up by the address they actually wrote from finds them. Contacts turn up in Avi's global search, and typing `@` in chat offers them alongside your project handles. ## Duplicates A contact's primary email is the duplicate key within a source. Creating a second contact with an address already in the book is refused, and the refusal names the existing person — so the agent updates them instead of quietly creating a second row. ## Fields Name, primary and secondary emails, phones, company, title, website, addresses, social identities, tags, and notes — plus a `metadata` bag your apps can write to (see [Data Types](/docs/data-types)). --- # Wiki The **Wiki** is your organization's shared knowledge. Everyone in the org reads it, and so does every agent and every person's Avi, so the same facts and the same playbooks steer all of them. It is in every project and every [agent](/docs/agents). ## Three kinds of page - **Knowledge**: what is true about the company, such as products, pricing, policies, your voice, and where things live. - **Skills**: how a kind of work is done, step by step. Agents follow these playbooks when the work comes up. Your own skills live only here. - **People**: one page per person in the org: their role, what they own, what they approve, and how to reach them. Everyone gets one when they join. ## How Avi uses it When a topic comes up, Avi searches the wiki and reads the page it needs before it answers or acts, instead of guessing. Ask it things like "what's our refund window?" or "who approves discounts?" ## Owners - Every page has **one owner**. Only they change it, delete it, and choose who can read it. A person page belongs to its person. - Your Avi can create, change and delete your pages when you ask it ("update the pricing page, the Team plan is $60 now", "move the onboarding skill to Knowledge"). It asks before deleting, and it can't change anyone else's. - Want a change to someone else's page? Ask its owner. - A page whose owner left the org has **Become the owner** for anyone who can read it. ## Who can read it A page is either readable by **everyone in the org** (the default) or **private**. The owner switches it under **Sharing**. A private page stays in the project it was made private in: in your own project only you see it; in an agent's, only that agent's people do. ## Good to know - Deleting a page is permanent and always asks first. - Skills written before the wiki are private skill pages in the project that had them. - Avi also has **Native Skills** built in: how to work on Word documents, Excel workbooks, PDFs and e-signatures, keep this wiki, set up automations, research, arrange meetings, collect requests, and the Code app's Spec, Build and Review. They come with Avi, are not wiki pages, and can't be edited. See [Native Skills](/docs/background-agents#native-skills-work-avi-knows-how-to-do). --- # Automations Automations are standing work Avi sets up for you. Each one is a prompt plus an optional schedule or event trigger: it runs on its own, without tying up your chat, and reports progress as it works. They're how Avi does anything recurring, long-running, deferred, or reactive. Two words carry the whole model: - An **automation** is the standing work: its instructions, tools, triggers, budget — and its own working state and **status**. It's active, it needs you, it succeeded, it failed, or you stopped it. - A **session** is one *sitting* — what one wake did, and any replies to it. Sessions are mechanical: one finishes, errors, times out, hits a limit, or is stopped; the automation's verdict never hangs on a single sitting. When one piece of work should fan out into several — one per customer, per repo, per email thread — each is its own automation, with its own thread and its own verdict. One piece of work, one automation, one story. Work like that belongs in an [agent](/docs/agents): a project the org owns, where "onboard Acme" sets up Acme's automation and an automation that finds new customers sets up each one's. ## Creating one You don't build automations by hand — you ask for them in chat: - "Every morning at 7am, email me a summary of new GitHub issues." - "Check if our homepage is up every 15 minutes; page me if not." - "Keep an eye on these five competitors and tell me whenever one changes its pricing." - "Whenever a PR comes in on my repo, triage it — if it needs my review, make a task for it." - "Whenever my grandmother emails me, draft a warm reply." - "Ask everyone on the team for their shirt size today, then nudge whoever hasn't replied each morning until they all have." Avi sets up a standing automation only when you ask for one or the work is ongoing: later, on a schedule, whenever something happens, or until other people reply. A one-off question or document never becomes a standing automation. Avi answers it in the conversation, or hands it to a [one-time automation](#one-time-automations) in the background, whose answer comes back in the chat. **The result finds you — without cluttering the chat.** An automation never posts into your conversation. The card Avi put in the chat when it started the automation keeps itself current: the automation's name, where things stand, and — when it finishes — how it ended and what came of it. The full result lands in Updates, and the automation's own page has everything. You can follow up in chat directly ("ok, fix the second one") — Avi knows where each automation it started stands. In a voice session Avi tells you in one sentence when it finishes — it waits for a quiet moment and never talks over you — and "tell me more" gets you the rest. Automations that run on a schedule or a trigger report to Updates as they go. When a result is worth seeing — photos, pages it compared, a side-by-side table — the automation shows it on its update in your feed, not just describes it. The agent decides whether to answer you right away or hand the work to an automation, sets the schedule, and confirms — and the chat stays free while the automation works: progress lands in [Updates](/docs/updates), and you can ask for a status check any time. There's deliberately no form to fill in: describe what you want and when, and adjust it the same way ("make that weekly instead", "stop the issue digest"). **From chat**, Avi picks the tools the automation's work needs and switches them on when it sets it up, so it starts working on its first session (see [Tool access](#tool-access)). **From the panel**, **New** creates a draft — a dormant automation that opens on its **Setup** tab, where you fill in its name, when it runs, its instructions and tools, then press **Start**. Nothing runs and no trigger fires until you start it. ## Pre-made automations Some work is common enough that you shouldn't have to write it from a blank page. **New** in the Automations panel opens a picker: start from scratch, or pick a pre-made automation. It is added at once and opens on its **Setup**: its stages in the chart, its instructions a tab away. Change what you like, or nothing. It is added active with no triggers of its own: give it a schedule or triggers there or in chat, or message it, to set it working. A pre-made automation is an ordinary automation that arrives already written, and nothing more. Its **General** instructions and each stage's instructions come filled in with short, plain guidelines in your words, and what you read on the Instructions tab is everything it follows: there is no hidden playbook underneath, and no questions to answer. Edit a line and that is what it does. It also arrives with the tools its work needs switched on, such as your email, Drive and calendar, as far as your project has them. They show on its **Tools** tab like any other. The list offers: - **Client onboarding** — welcome a new client, set up their folder and contacts, get the engagement letter e-signed, take in what you need and book the kickoff, and hand them over ready to work with. The folder exists before the letter, and nothing is collected until the letter is signed. - **Document collection** — ask people for files, chase them, review what arrives, and file it in Drive. Each is written for one client or one collection. A project holds one of each — picking it again opens the one you have — so for one per client, make it a [group](#groups) (**⋮** → **Make it a group…**) in the [agent](/docs/agents) that looks after them, and add each client to it with who they are: their email address or domain, and what is known so far. ## The Curator Every project has one automation you didn't create: the **Curator**, which keeps the project's [Updates feed](/docs/updates#the-curator). It's always on, in every project, including new ones. - **It can't be stopped, archived or deleted.** Its menu has none of those, and Avi refuses them in chat too. - **Its setup is Avi's.** Its instructions, model and tools are kept up to date by Avi. Its **When it runs** reads **Always on**: it wakes two minutes after new activity arrives, never on a schedule. - **Its Instructions tab** shows **Built-in** (Avi's curation rules, read-only) and **Yours**, where you write what matters in this project's feed. Yours are added to the built-in ones, and win where the two disagree. - **Its Sessions tab** shows each pass it made over new activity. ## Stages Work that moves through steps shows them. Document collection goes **Ask → Collect → Review → File**; client onboarding goes **Welcome → Set up → Agreement → Intake → Hand off** (the folder exists before the agreement goes out, and nothing is collected until it is signed). Wherever a stage is named it wears the same small stage icon. - On the **Activity** tab, the stages are the page: one row per stage. A finished stage carries a green check after its name, under it what it came to in a sentence, and on the right when it was completed. The current stage shows where things stand, and when it is waiting on you the question and its answer buttons sit right inside that stage. Stages still ahead show what will happen in them. Colour is kept for what matters: orange when something needs you, green when it succeeded, a quiet red when it was unsuccessful, and the full red for an error. Work that is simply under way stays in plain ink. **Stages are the path to success.** Work moves through them one at a time, and a stage is only marked finished when its work was done. When the work cannot go on — an address bounced and there is no other, they declined, nobody answered — the automation ends as **Unsuccessful** right then and stays in the stage where it stopped: that stage is not marked finished, and nothing after it is. **Succeeded** is only reached from the last stage. So do not add a stage for things not working out ("No response", "Lost"); such a stage is refused. Say in the instructions when to give up instead. **Your own stages.** Tell Avi the steps when you describe the automation ("stages should be Introduction, Contract, Hand-off; the contract must be reviewed before it goes out") and it declares them, with your approval required where you said, and writes each stage's own instructions — what happens in it — which you can read and change on the **Instructions** tab under the stage's name. Renaming or reordering stages keeps what you wrote for each. You can also add, rename, reorder (the up and down arrows on a row) or remove them on the automation's **Setup** tab with **Add stages** or **Edit stages**, or ask in chat. Work already under way stays in its stage when the edited list still has it. A pre-made automation's stages are fixed and each comes with its own instructions, which you can read on the Instructions tab under the stage's name. Their approval switches, and anything you change under a stage, are yours and stay through every update of the automation. Only automations that have stages show them. An automation without stages shows a plain timeline of what it did, and no stage display anywhere. ### Needs my approval Each stage has one switch on the Setup tab: **Needs my approval**. When it is on, Avi prepares that stage's work, then asks you to sign off before it moves on. This switch is the only place approval lives: when an automation has stages, it asks for your approval in a stage you switched on and nowhere else, so turning a switch off stops the asking even if its instructions still mention checking with you first, and Avi puts an approval you ask for on the stage's switch rather than in the instructions. The question arrives like any other (see [When an automation needs you](#when-an-automation-needs-you)), with one-tap choices that include approving everything at once. This is enforced, not just requested: until you answer, the automation cannot move to the next stage or finish as succeeded. Document collection needs your approval in **Ask** (the first emails) and **Review** by default; client onboarding in **Welcome** (before the first email goes out) and **Agreement** (before the engagement letter goes out for signature). Switch a gate off when you trust the work, or on when you want a look. ## An older automation with a Coding Sandbox Coding work runs in the Code app, never in an automation. An automation made before that may still have a Coding Sandbox. It keeps it and runs exactly as before: its Setup shows the sandbox read-only, and it can't be duplicated. ## Automations and Word documents An automation brings the same Word know-how Avi uses in chat. If its instructions involve documents ("every Friday, draft the status memo as a Word document"), or you message it a document task, it works on the document itself with the same care: one tracked change per edit, totals worked out, no PDF unless asked. Nothing needs to be turned on. ## One-time automations A **one-time automation** is one request Avi does in the background and reports back on: it runs once, finishes on its own, and its answer returns to the conversation you asked in. It is an automation like any other — in your Automations list, opened and stopped the same way. - **Ask in chat and Avi starts one.** "What does Feed.fm allow in a screen recording?" becomes a one-time Research automation. "Go through the contracts folder and summarize each one" becomes a one-time automation too. Avi replies in one line and the chat stays free. - **Each one is a card in the chat.** The card carries the automation's name and the step it is on. When it finishes, the answer follows: the short version first, **Show more** for the specifics and sources, and a tile for each file it created or changed. In voice mode you hear the short version, and a file it made opens on screen. - **It is in your Automations list.** While it works it sits in **Live**; once it is done it moves to **History**. Click its card in the chat and it opens there, like any automation: Activity, Setup, Instructions (the request it was started with), Tools and Session. - **You can keep talking to it.** Type into its message box. While it is working, your message changes its course at its next step. After it has finished, your message continues the same work: it remembers everything above and picks up from there, and its result in the chat updates. (It has nothing to wait on, so there is no **Resume** for it — a message is how it picks back up. Its model and tools are Avi's for that kind of work, shown on Setup without a switch.) - **It always reports.** It ends by reporting its result or by asking you something, never by going quiet. If it stops without reporting, it is marked failed with what it last said, so the card in the chat tells you what happened and you can start it again. It is never left sitting Active: if its session crashes or is stopped, or it cannot start at all, it ends there — failed or Stopped, with why — and moves to History; a message picks it back up. - **It asks you.** When it needs a decision only you can make, it asks as a **Needs you** question on its card. - **Only the chat starts them.** Your automations don't start one-time automations; Avi starts them from chat when you ask. Work an automation started before this changed still shows as a tile in the session that started it, and that automation still answers it. ## Native Skills: work Avi knows how to do Avi comes with **Native Skills**: know-how for particular kinds of work, built in and maintained by the Avi team. Avi brings the right one to a request without being asked. They are not pages in your [Wiki](/docs/wiki), where the skills are your own. Where the work runs depends on the work: - **Word documents** are done in the conversation (see [Files](/docs/files#word-documents)). - **Excel workbooks, PDFs and e-signatures**, and keeping the [Wiki](/docs/wiki), are done in the conversation too. - **Research** runs as a one-time automation and reports back in the chat. - **Meetings** and **Requests** wait on other people for days, so each request becomes an automation of its own. It appears in the Automations list, named for what you asked, with **Meetings** or **Requests** beside its name, and it reports to your feed as it goes. Avi sets it up; there is nothing for you to configure. Research, Meetings and Requests bring their own tools. They do not need those tools turned on in your project's Tools list. Their playbook and model are Avi's, so their **Setup** tab shows them without letting you rewrite them; you can still change when they run, message them, stop them, or archive them. A request is one piece of work, however many meetings or items it holds. Settings such as a tone, a default meeting length, or additional guidance are filled in per request from what you asked and your project instructions; a note on one meeting ("keep the coffee more formal") overrides the setting for that meeting. Put standing preferences in your project instructions and every request honors them. ### Research Research answers a question that needs more than one quick search. It checks what your project already has first (an earlier note, a thread about it), searches the web several ways, and reads the pages that settle the question: the company's own terms, docs, pricing and help pages, not someone else's summary of them. Its answer leads with one sentence that stands on its own, then the specifics with links to where each came from, then what the sources leave open and who could settle it. It reads only: it never signs up, buys, sends, or contacts anyone. ### Meetings Meetings puts meetings on your calendar with the people you name: one request, however many meetings and people it holds. It reads your availability, writes to each person once by email covering everything you need from them, handles the back and forth in that one thread, and books each meeting with a Google Meet or Zoom link when a time is agreed. It uses your Calendar, Email, and Contacts apps and the web. - Starts every session by re-reading its email threads and anything the other people sent outside them, so nothing that arrived between sessions is missed. - Follows your project instructions (your name, company, signature, who to copy, hard rules) ahead of its own defaults. - Proposes two or three concrete times inside your working hours, with the timezone spelled out. Working hours belong in your project instructions; without them it assumes 9:00 to 17:00, Monday to Friday. - For an in-person meeting, checks the place on the web first: that it still exists, that it is open at the proposed time, and its exact address, which goes on the invitation. It never proposes or books a place it could not verify. - Writes as you: first person, your name, your signature from the project instructions, sent through your Email app. It presents itself as your assistant only if your project instructions or your request say so. You do not click Send; asking for the meeting is the instruction. - One thread per person, everything in it. Three meetings with the same person are one email with three sets of options, and every reply stays in that thread. It continues an open scheduling thread with that person rather than starting another. - Wakes up the moment a reply lands (it listens for mail from everyone named in the request from the start), checks your calendar again, and either books or offers new times. - Keeps to the meetings. If the other person raises anything else — a question for you, a request, an invoice, a personal matter — it does not answer for you: it brings it to you as a **Needs you** question with a suggested reply you can approve, change, or dismiss (through the automation that started it, when one of your automations did). - Books each meeting on your default calendar as it is agreed, with the attendees invited and the video link attached, nudges once after two quiet working days, asks you before giving up, and marks itself done when everything in the request is booked. Per request, whoever starts it fills in its settings: **Timezone**, **Default meeting length**, **Buffer between meetings**, **Video** (Google Meet, Zoom — needs a Zoom account connected in the Calendar app's settings — or no link), **Tone** (friendly or formal), and **Additional guidance** (free text for that request). To steer a request already under way, message it (or the automation that started it): Meetings replies in the same email thread it started, never a second one. The playbook itself is not editable; additional guidance is how you shape it. ### Requests Requests gets things sent in that other people owe you, and keeps after them until every item is in: documents (a signed form, last year's return, a countersigned contract), confirmations (an explicit yes or no), and details (an address, bank details, references). Chasing an overdue invoice, a late deliverable, or an unanswered approval is the same job. It uses your Email and Contacts apps, nothing else. - **One request, one thread per set of people.** Hand it everything at once — every item, who owes it, the exact wording, the deadlines. An item can be owed by one person or by several who can each provide it (a client and their advisor), and people you name together get one thread with all of them on it. It writes each set one email covering all they owe, grouped by due date with the soonest first, why, how to send it (an attachment, a link, or wherever you tell it people should upload), that they should send each thing as soon as they have it rather than waiting for the rest, and that anything which does not apply just needs saying — and keeps that one thread going. Items have their own deadlines when you give them: reminders lead with what is due soonest and always mention what comes later. - **It keeps the checklist itself.** Its own working state is the record of every item: asked, arrived, received, declined, or waived, with what came in and when. It never relies on a folder or a mailbox to know where things stand. - **A reply from anyone counts, whoever sends it.** People delegate: a colleague answers from their own address, an assistant or a financial advisor at another firm writes in, someone forwards the file from a personal account. Requests wakes on three things: mail from the people it asked and their organizations, anything continuing a conversation it started (from any sender), and mail Avi judges to be about the request even from an address it has never seen. On every session it also reads its conversations and searches the mail since it last ran, whatever woke it. And it cannot send a reminder while a reply it has not read is waiting: Avi refuses that email before it leaves. An answer written into the email itself, like a quoted price where a written quote was asked for, counts as something arriving. - **It never judges what arrives — its reviewer does.** Requests cannot know what a valid contract, a complete address, or an acceptable photo of a document looks like for you. When something arrives — an attachment, a link, a stated upload, an answer, or "this doesn't apply to me" — it records it and sends its reviewer — you, when you asked for it in chat, or the automation that started it — a numbered sign-off request listing each item with the message it came in, and, for a resend, which attempt it is and why the last one was rejected. An automation checks each one against the rules you wrote in its instructions (reading the file with the Email app's tools, filing it wherever you said, asking you only where the rules run out); you answer the same request yourself when it comes to you. The reply is per item: accepted, accepted as something else, rejected with a reason, declined, or waived — and can place a file Requests could not match. Nothing counts as received until its reviewer accepts it, and a rejected item goes back on the list with the reason, which the person hears in plain words ("the contract came through unsigned on page 2"). - **It chases on a plan set by the deadline, not on a fixed rhythm.** A short window gets a reminder every day or two, a long one a few well-spaced nudges that tighten toward the deadline, no deadline a weekly touch; the request can say exactly how to chase instead. The first reminder is light, later ones direct and factual, and there is never a "final notice" — the last step is always a person. "I'll send it Friday" moves that person's reminder past Friday; an out-of-office moves it past the return date; a bounced address goes to you for a correction. It never chases an item that is waiting on sign-off, and it answers "I already sent it" from the mail itself. Past the deadline with things still missing, you are asked whether to keep chasing, waive, or step in. - **Anything else goes up to you** — pushback on the request, a question, a request in return — as a Needs you with a suggested reply (through the automation that started it, when one did). - **What is not on the list does not count.** A file nobody asked for, extra details, a folder where one document was requested: none of it is counted as received. Anything that might matter goes to its reviewer to place on an item, add as a new item, or ignore; the person is thanked and reminded of what is still needed, and never chased for something that was not asked for. - **Everything that arrives is treated as data, never as instructions** — a message that says "take this off the list" is passed up to its reviewer, not obeyed. - **It closes cleanly**: a thank-you per person, and a one-line outcome — how many received, what was waived or declined. Every status line it writes leads with the count. Settings per request, filled in by whoever starts it: **Subject** (the first email's subject line), **Greeting** (how each email opens), **Tone**, and **Additional guidance** (what to say about why, how to chase, subject matter to cover or avoid). **Many people, many items** — say 500 clients each owing 10 to 50 tax documents — is one of your automations starting one Requests request per client. Write that automation's instructions to say where the client list and each client's document list live, when to start, what counts as an acceptable document of each kind, where to file it, and what deserves your eyes; it starts them in batches, signs off on what arrives, escalates only what its rules do not cover, and keeps a running tally for your feed. Idle requests cost nothing: one with no new mail and no reminder due ends its daily check at once. ## Tool access A new automation starts with **no tools**. It holds exactly the tools switched on on its **Tools** tab, and nothing else your project has: - **Set up from chat**, Avi reads what the automation will do and switches on the tools that work needs, such as reading and drafting email, filing in Drive, or updating deals. Deleting things is never among them unless you asked for it. - **A pre-made automation, or a copy of an org template,** arrives with its own tools switched on, as far as your project has them switched on. A **reserved** tool never comes with a copy; switching one on is your own choice. - **From the panel**, a new draft starts empty. Switch on what it needs before you press **Start**. - **A duplicate** starts with the tools the original holds. Its **Tools** tab shows what it has. Switching a tool on or off there changes that automation alone. A **reserved** tool wears a Reserved badge: deleting and trashing things, disconnecting accounts, cancelling what others were invited to, and changing the project's own settings. Switching one on is your explicit grant to that one automation. Automations made before new ones started empty keep how they worked: everything your project has switched on, except the reserved few, plus whatever you changed on their Tools tab. A tool you switch on in Project Settings later still reaches them. An automation that runs into a wall does not improvise around it: it searches what exists in the project and asks you for the specific tools it needs, with a one-line reason. That arrives as a **Needs you** with Approve and Deny buttons, in the feed and on the automation's Activity tab, each requested tool shown as a tool chip. Approving grants those tools to that automation only, and its next session has them. A single send is different: an automation without the send tool asks you to approve that one send, and Avi performs it as you (see [When an automation needs you](#when-an-automation-needs-you)); the automation holds no new tool afterwards. A tool you have switched off in Project Settings → Tools reaches an automation only this way, by your own switch under **Tools**, or when Avi names it among the automation's tools in chat, while setting it up or when you ask. You can also set the whole set in chat: "give this automation only the CRM and web search tools" sets exactly those switches on its **Tools** tab, everything else off, and the reply lists what it now holds. Ask what it holds first ("what tools does it have?") when you want to trim rather than rebuild the list. Whatever the grant, an automation never manages other automations, and Research, Meetings or Requests work it starts brings its own tools. An automation holds **one open question at a time**. If what it needs changes before you answer — another tool, a different choice — it rewrites that same question in place, so you see one card that changes, never a pile of cards. A tool request that grows becomes one request for all the tools. When it no longer needs an answer, it withdraws the question. ## The Automations panel Open the **Automations** tile on the rail (the dock on a phone). One list holds every automation — the ones you set up, and the [one-time automations](#one-time-automations) Avi started from chat — in three columns: **Automation**, **Status**, and **Last ran**. Under each name sits a one-line account of what it last did, and under that **what starts it**, one tag per trigger in plain words — "Weekdays at 7 AM", "Email from acme.com", "Replies to its emails". An automation that reacts to several things shows several tags; they wrap, and hovering one shows the detail behind it. The list opens on **Live**: everything standing or in hand, ordered by what needs attention — **Needs you** first, then what is working, then what is paused, then the rest, most recent first. **History**, chosen under **Filter** → Show, holds what has finished: one-time work that succeeded or failed, and automations with nothing left to start them. An automation you stopped that still has a schedule or triggers stays in Live, one click from resuming. Each row ends with a **⋮**: **Stop**, **Start** or **Resume**, **Archive** (or **Unarchive**) and **Delete** — the automation's own menu, which asks first before it stops, archives or deletes one. A group's row has **Stop all** and **Delete**. Search by name; filter by **State** and by **Runs** (One-time, On events, On a schedule); the headers sort. Archived automations are under History's State filter. When the list is narrow (on a phone, or beside an open automation) its header shows only **Filter**. **New** opens the picker for a [pre-made automation](#pre-made-automations) or a blank draft. Open an automation and its tabs sit directly under the breadcrumb. An automation has **Activity**, **Setup**, **Instructions**, **Tools** and **Sessions** (a one-time automation's reads **Session**). - **Activity** — the automation's own account of its work. With [stages](#stages), it is the stage table. Without them, it is a timeline of what changed, newest first: options sent, a reply received, booked, a question for you, a failed session. The automation writes these itself and only when something actually changed, so the timeline stays readable however many sessions ran. Each session's newest entry has a **View session** link to that session's transcript. When the automation finishes, its final entry carries the whole outcome, not a one-line summary. If it is waiting on you, the question appears inside its timeline entry and you answer it there. When the automation is tracking several things — each document it requested, each person it asked — it shows where each one stands as a short table inside the timeline entry that reported it: each item with its state above it (waiting, working, done, blocked, or needs you), and a link tile when it points to a file or record — the tile opens it inside Avi. The newest table is shown open; earlier ones open with **Show what it looked like**. That table is the current picture and says when it was last updated; earlier versions stay in the timeline behind **Show what it looked like**. The same table appears on the automation's row in [Updates](/docs/updates). To message the automation, use its **Sessions** tab (see below). Every entry is its own point on the timeline, always shown; a session's newest entry carries its **View session** link. A session that finished without writing anything still leaves one line, what it did in a sentence, with its **View session** link. A question for you is answered at the top of its point, above anything it lists. Several links under one line show as one compact list. - **Setup** — everything that defines the automation. Setup, **Instructions** and **Tools** are tabs of the automation, beside the others; say any of them, or what sits on them ("the schedule", "the prompt", "tool list"), and the record opens there. Every label's description sits behind a small info mark to its right: press it to read, and close with the X. **Setup** is the automation's fields, in this order: - **Name**. Rename it here and save, or ask in chat ("rename the outreach automation to Email Avi deck to SMBs v1"). It stays the same automation, and its row in Updates follows. - **When it runs** — every trigger as a tag, the same tags as the list ("Weekdays at 7 AM", "Email from acme.com"). An app's event is named in full ("CRM deal created or stage changed"); only a subscription to everything an app emits reads as activity, and says so ("Any CRM activity"). Hover a tag for the exact event names behind it. An automation with no trigger of its own shows **When started** and **When messaged**. When the automation started work of its own before automations stopped doing so, that work's triggers are listed too, each naming the work it belongs to. - **Stages** — only on an automation that has them: each stage, its **Needs my approval** switch (see [Stages](#stages)), and an **Instructions** button that opens that stage's own instructions. A stage is its name and its instructions; there is no separate description to keep in step. - **Model and limits** — the model, max daily spend, and spend so far. Every automation runs on **GLM 5.3 Flash** unless you pick another model here, or name one when you ask Avi to set it up. That includes the one-time automations Avi starts, and Research, Meetings and Requests. The daily spend ceiling is the one limit on how much an automation may use; there is no cap on sessions per hour. In an [agent](/docs/agents), up to 10 sessions run at once across its automations, and the rest wait their turn. - **Advanced** — collapsed, and shown only for a legacy **Coding Sandbox** (read-only — see [An older automation with a Coding Sandbox](#an-older-automation-with-a-coding-sandbox)). Work an automation started earlier shows as tiles in its sessions, and never asks you directly; its questions come to you from the automation itself. **Instructions** is the standing brief, with the whole tab to write in. Edit it, save, and the next session uses it. When the automation has [stages](#stages), a picker above the editor chooses whose instructions you are editing: **General**, or one stage by name, which has instructions of its own: what happens in that stage, in as much detail as you like. Each stage in the Setup chart is a link to its instructions. Automations you already had keep their instructions exactly as they were; a stage's own instructions start empty until you or Avi write them, and a pre-made automation's arrive filled in. Work Avi started for a request — a one-time automation from chat, or Meetings and Requests — shows **What it does** here instead: the request it was started with and your answers. **Tools** is the table of tools it holds, each with a switch (see [Tool access](#tool-access)). - **Sessions** — every time the automation wakes (its schedule fires, an event arrives, you message it) it starts a session of its own. The list shows one row per session, newest first: what it did, what woke it, how it ended, and when it started; search it, or filter by what woke it and how it ended. Open one to read it like a chat — every reply and tool call inline, streaming live while it works, with a stop button and a message box. A message in an open session is a **reply**: it continues that session, which picks up exactly where it left off. **Message** above the list starts a new session with your message. Each new session starts fresh: what carries from one to the next is the automation's [working state](#working-state), its status and its instructions. A one-time automation is the exception: its **Session** tab is one conversation, and every wake continues it. A table it showed earlier is a picture of that moment: a row that was still being worked on then wears a still mark, and only a table being shown in a session streaming right now has a turning one. Under the tabs, one status row shows the state badge and when the automation last ran — on every tab but Instructions and Tools, which have none. On the right sit **Save** (on Setup in that row, and on Instructions at the right end of the row that picks whose instructions you are editing, with the ⋮ menu beside it; Tools has none, since its switches apply as you flip them — the one Save for everything typed on that tab: name, instructions, spend, what each one is called, stages being edited, a pre-made automation's answers; switches and menus apply as you flip them), **Start** (on a draft), and the ⋮ menu. The ⋮ menu holds **State** (the automation's own working notes — see [Working state](#working-state)), **Stop automation…** or **Resume automation**, **Archive automation…** (or **Unarchive automation**), and **Delete automation…** — permanent; the confirm says what is lost. ### How an automation shows up in Updates An automation's work reads as one story, and all of it is in one place: everything an automation does, and everything that comes back about it, lands on its row. Its row in [Updates](/docs/updates) carries what it did, the records and files it created, the messages it sent, its questions to you and its outcome — not a separate row for each. An automation has **one row** in Updates — in an [agent](/docs/agents), each customer's automation has a row of its own: - **Everything it does** is a line on that row. Routine progress stays quiet, and the newest line is rewritten as things move. - **Questions** count on the row ("Needs you (2)") and are answered on the automation's update page: open the row, and what is waiting on you is listed at the top, a line each, above the story. You are notified for each question. A question also appears on any other topic that carries that automation's work, so wherever you read the story, the question is there to answer. - **Outcomes** — the automation succeeding, or ending unsuccessful — and a run hitting an error are lines on the row that notify you. **Automations chain.** Tell Avi "when onboarding finishes, start the kickoff automation" and the second starts on the first's conclusion, with the result and what it made as its first message. The tag on Setup reads **After Client onboarding**, and a chain can never loop back to itself. **One-time automations share one row.** Every [one-time automation](#one-time-automations) Avi starts from chat reports its result as a line on the project's single **One-time automations** row, led by its name, so you can go back to it without a row each. It does not notify you there: its answer is already in the conversation. Avi decides which of these a development belongs on, the same way it curates the rest of the feed; if something is plainly part of another matter you follow, it goes there. **When an email bounces.** If an email an automation sent cannot be delivered, the automation that sent it is told right away: which address failed and why. It does not have to notice the mail system's notice in your inbox, and no other automation is disturbed. This works the same whether the mail went out from your Avi address or a connected account such as Gmail. What it does next is up to its instructions (try another contact, stop, or ask you). **When a run fails.** A run that stops on an error is never silent. It appears on the automation's row in Updates as "Error: …" and notifies you; on the Automations list its line reads "Session failed: …" with the reason, and the failure is the newest entry on its **Activity** tab. To run it again now, press **Message** on its **Sessions** tab or ask in chat. The automation stays active and runs again on its next trigger. Results also reach you the normal ways: anything an automation produces (notes, emails, messages) lands where it was sent, and its activity is folded into the project's Updates feed. ## Groups When the same work happens for each of many things — onboarding each new client, screening each candidate for a role, chasing each unpaid invoice — make it a **group**: automations that share **one setup** while each does its own work. - **One setup, changed in one place.** The group's instructions, stages (with their **Needs my approval** switches), tools, model and max daily spend are shared by every automation in it. Change them on the group and every one of them runs on the change from its next session. - **Each one is its own.** Each automation in a group has its own name ("Acme Dental"), its own triggers (its client's email, its own schedule), its own conversation, its own stage and its own outcome. - **A group calls them by name.** You tell Avi what one of them is — a client, a candidate, an invoice — and the group uses that word everywhere: "6 Clients", the **Clients** tab, "Stop Client…". **Making one.** **New** → **Start a group**: give it a name ("Client onboarding") and say what one of them is ("client" / "clients"). The group opens on its **Setup**, empty, ready for the instructions, stages and tools they will share. Or ask in chat: "set up client onboarding as a group — one per client, starting with Acme Dental". An automation you already have can become the first of a group: its **⋮** → **Make it a group…**, or ask Avi ("make the onboarding automation a group of clients"). It keeps its name and triggers, and its setup becomes the group's. Its tools become exactly the ones it holds now, set for the whole group (switching a tool on for the project no longer adds it). Save any changes first. An automation copied from an org template can't start a group — start one fresh instead; one published to your org, or archived, can once you unpublish or unarchive it. **Adding one**: **New** on the group's page asks for its name ("Cedar Street Dental") and opens it — it runs on the group's setup and starts when you start it or message it; ask in chat to give it triggers of its own. Or ask Avi ("onboard Cedar Street Dental") — once the group has its instructions. In an [agent](/docs/agents), an automation that finds new clients can add each one to the group itself. **In the Automations list** a group is ONE row: its name with a stacked icon after it, and under it how its automations stand — "6 Clients · 1 needs you · 3 under way · 1 succeeded". Its status is theirs: **Needs you** when one of them needs you, **Working** while one is working. It stays in the live list while any of them is live — including one you stopped that still has triggers. Searching the list finds a group by the name of any automation in it, and **Filter → Runs** shows a group under the triggers its automations have. **The group's page** has four tabs: - **Clients** (named for what the group handles): one row per automation — its name, where it stands, what wakes it, its stage, its status and when it last did something — what needs you first. Search, **Filter** (by status, by stage) and **New** sit above the list; each row's **⋮** stops, starts, archives or deletes that one. - **Setup**: the group's name, what one of them is called, its stages and their approval switches, its model and its max daily spend. The spend is a ceiling **each** automation in the group has of its own, not one shared pool. When one of them reaches it and you raise it, the raise is the group's: every automation in it gets the new ceiling. - **Instructions**: the general instructions, and each stage's own. - **Tools**: what every automation in the group may use. The group's **⋮** stops every one of its automations, or deletes the group with all of them. **One automation of a group** opens under "Automations / Client onboarding / Acme Dental" with three tabs: **Activity**, **Sessions**, and **Triggers** — what wakes it, which is its own. Its setup is the group's: ask Avi to change it on the group, or open the group's page. A group keeps its setup when its last automation goes; delete the group itself from its **⋮**. ## Templates A **template** gives someone their own copy of your automation. Publish an automation to the org and anyone here can copy it: the copy is theirs, runs as them, with their accounts, and starts dormant on its own Setup page — so a sales lead can design Client outreach once and every rep can run it on their own territory. - **Publish** from the automation's menu: **Publish to the org…**. Write the one line people see in the picker. Your triggers, model and spend are not part of the template; what people get is the name, the instructions, the stages with their instructions and approval switches, and the tools it holds, as far as their project has them switched on and never a reserved one. - **Find and copy** under **New automation**: pick **Your org** beside the search to see only what your org has published, or leave **All** for everything. Picking one makes a copy of your own in your project, which opens on Setup. - **Edit** a template by editing the automation you published. Your later edits reach every copy wherever its owner has not written their own: a stage's instructions someone rewrote stay theirs, a switch they flipped stays flipped, a list they reordered or added to keeps their shape. The listing's one line is edited from the automation's menu (**Edit the template…**). - **Unpublish** from the automation's menu (**Edit the template…** → Unpublish). The listing goes; copies people already made are theirs and stay. ## Messaging an automation A standing automation isn't fire-and-forget — you can talk to it. Open it in the panel, go to **Sessions**, and reply in a session ("why did you email Sam twice?") or press **Message** to start a new one ("also include closed PRs"), or just tell the assistant in chat ("tell the issue-digest automation to also cover the docs repo"): - If it's working right now, your message reaches the session in flight — it changes course mid-session instead of starting something new. (Sandbox sessions are the exception: the message becomes the next session.) A message that arrives just as a session ends isn't lost: it continues that session right away. - Otherwise it runs right away to act on it: a reply continues its session; a new message starts a fresh one. - A tweak sticks for future sessions: when you tell it to change how it works ("skip weekends from now on"), it records that in its working state, which every later session reads. - A message can even change what *triggers* the automation ("also run whenever Bob emails me", "stop watching that repo") — the automation updates its own event or feed subscription in response. Messaging a concluded automation **resumes** it — the same automation picks back up with your message as its next input: a reply in one of its sessions continues that session, a new message starts a fresh one (see [States](#states)). Adding a new question to finished work is exactly this: message the automation, don't start a copy. ## When an automation needs you Sometimes an automation hits a decision that's genuinely yours — "both Tuesday 2pm and Thursday 10am work for everyone; which do you prefer?", "this contact has two emails on file; which is current?" Instead of guessing or silently stalling, it **asks**: - The question lands in your [Updates feed](/docs/updates) (and notifies you) as a "Needs you" mark on the automation's own row. Open the row: everything waiting on you is at the top of its page, above the story — the question, what the decision needs in the automation's own words, the draft or record it is about as a tile, the controls, and one line saying what happens if you never answer ("If unanswered in 3 days: I'll propose Tuesday 2pm"). Press **Answer** on a question to see its choices and type your own answer. The automation's **Activity** tab shows exactly the same thing inside its "Needs you" entry on the timeline — it is always the newest entry, because asking is the last thing a session does. - **Approving a send.** When what an automation needs is your go-ahead to send something — an email it drafted, a meeting it is booking, a message it is posting — the line says exactly that ("Send “Welcome aboard” to alex@drumsinc.com"), the draft sits under it as a tile that opens it in its app, and two controls follow: **Approve** sends it, **Decline** takes an optional reason the automation reads. The automation never sends it itself: on Approve, Avi performs that one send as you, and the line settles to **Sent**. If you open the draft and send it from the Email app instead, the line settles to Sent on its own and the automation carries on. Nothing goes out until you approve, and a request you leave unanswered expires without sending. To make this the only way out, switch the send tool off under the automation's **Tools** tab; the automation then asks for every send. - The automation shows **Needs you** in the panel until you answer. - Your answer resumes the work immediately: a session that is working gets it at its next step; otherwise a new session starts with your answer in hand. Replying in plainer ways works too: messaging the automation (in the panel or via chat: "tell the scheduler Tuesday works") counts as the answer. - Every question comes with a deadline and a stated fallback — what the automation will do if you never answer ("if I don't hear back in 3 days, I'll propose Tuesday 2pm"). An unanswered question quietly becomes that plan, never a stuck automation. The feed shows how each question resolved: answered, expired into its fallback, or withdrawn (the automation finished or was stopped first). A one-off that asked a question waits for the answer (or the deadline) before it wraps up. Automations are told to ask sparingly — routine judgment calls stay theirs; questions are for decisions only you can make. ### When the decision is someone else's An automation can put a question or an approval to another member of your org. Name them by their **handle** — the @name on their identity in the org — in its instructions: "escalate pricing to @matt" is enough. From one of your own automations, the row lands on **their** Updates feed, exactly as it would on yours: the deed, the reason, Approve and Decline. Only they can act on it; on your feed it shows who it is assigned to, with their name, until they act. From an [agent](/docs/agents), the question reaches them as a message from the agent instead. When they approve an action, Avi performs it **from their account** — an email goes out from their mailbox and sits in their Sent folder. If they decline, their words come back to the automation like yours would. ### Reviewing many at once When the same decision applies to many records — move each of these deals, send each of these drafts — you get a **review list** that fills as the automation works: one row per record, the reason it is on the list under it, a chevron for the full reasoning, **Open** to look at the record, and **Approve** or **Decline** (with an optional reason) for that row alone. **Approve all** and **Decline all** handle the rest in one go. A row you have reviewed leaves the list, as does one whose deadline passed; the header keeps the count ("2 of 5 reviewed · 1 unanswered") and "Show 3 rows" brings them back if you want to look. The header says how many are left and what happens to rows you never answer; rows you have handled stay in the list, dimmed, until the whole review is done. Avi performs each approved change for you the moment you approve it; nothing happens to a row you decline or leave. The automation keeps working while the list is open — it is a proposal, not a question — and hears back once, after its run has ended and every row is settled or past its deadline, with what was approved, what was declined and why, and what was left for its fallback. ## Working state Automations keep private working notes — where they left off, what they are waiting on, decisions they've made, and standing directives you've messaged them ("skip weekends from now on"). Each session starts fresh, so these notes are what carries from one session to the next. This state never touches your Tasks, Drive, or other apps; it belongs to the automation alone. You can inspect it any time: open the automation's ⋮ menu and choose **State** — useful for seeing exactly what a months-old automation believes it's doing next. ## States **The status belongs to the automation.** A session doesn't carry a verdict — it's one sitting, and a bad sitting doesn't sink the work. Every automation shows exactly one state, everywhere it appears — the list, its own page, and the Updates feed all use the same vocabulary: - **Needs you** — the automation asked you a question and is waiting on the answer (see [When an automation needs you](#when-an-automation-needs-you)). This outranks every other state. - **Working** — a session is executing right now (you can watch it live). - **Paused** — the daily credit budget is spent; new sessions wait for tomorrow. Resumes on its own (or ask in chat to raise the budget). - **Active** — live: watching its triggers or waiting on its schedule, nothing running at this moment. - **Not started** — a draft: created with **New** and never started. Review its **Setup**, then press **Start**. - **Succeeded** — done for good. A one-off succeeds when its work does; a standing automation succeeds when it decides its work is complete ("watch for X, then report") or when you tell the assistant to mark it done. - **Unsuccessful** — done, and the goal was not reached: the automation decided for itself that it could not go on (the address bounced and there was no other, they declined, nobody answered after the last follow-up). It always says why. This is an ordinary outcome, shown in a quieter red. - **Error** — the automation broke and the platform ended it: a one-time run that crashed, or five failed sessions in a row. Shown in the full red, with the error. (A single session that hits an error is also marked **Error** on its Session and in Updates, but the automation stays active and runs again on its next trigger.) - **Stopped** — done because you chose to end it (**Stop automation…**, or ask in chat). Not a failure — just your call. - **Archived** — put away. **Archive automation…** ends it if it was running and keeps everything (sessions, working state, questions, its topic in the feed) while taking it out of the list, search, and the assistant's view. Pick **Archived** in the State filter to see archived automations; **Unarchive automation** brings one back with the state it had, and you can resume it from there. Succeeded, Unsuccessful, Error, and Stopped mean the automation is **dormant** — its schedule and triggers stop firing, and nothing runs on its own again. But a concluded automation isn't sealed: **you can resume it**, and the *same* automation continues — same working state, same sessions list, same topic in your feed. Message it (from its **Sessions** tab, or in chat — sending resumes it with your message), choose **Resume automation** in its ⋮ menu, or ask in chat. Resuming clears a failure and starts the reliability clock fresh; resuming an automation with a schedule arms the schedule again. This is deliberate: one piece of work should live in one automation — resuming keeps search, the Updates feed, and the assistant all pointing at the same story instead of a trail of near-duplicates. When you are done with an automation for good, archive it rather than deleting it: nothing is lost, and it can come back. When an automation reaches a verdict — Succeeded or Unsuccessful — that always lands in the project's [Updates feed](/docs/updates) too, under the automation's own topic, headed by a one-line account of the outcome: "Succeeded: *what it accomplished*", or "Failed: *why*". The full outcome sits beneath that line, in the Update and on the automation's Activity tab, so you can read what it found or did without opening a session. You never have to open the panel to learn that standing work finished or broke, or what it did. (Stopping an automation yourself doesn't post one — that's your choice, not news.) ## Max daily spend Every automation carries one ceiling — its **max daily spend** — so nothing can quietly run away. It is a guardrail, not a target, and it is deliberately high: **100,000 credits a day** for every automation, on every model — the ones you set up, the [one-time automations](#one-time-automations) Avi starts from chat, and Research, Meetings and Requests alike. There is no other limit on what an automation spends. - As a session approaches the day's ceiling it's told to wind down: it finishes the item in hand, saves its progress, and ends cleanly. - Once the ceiling is reached, new sessions wait until the next day and the automation's Activity tab records the pause. When the ceiling is one you set lower, a **Needs you** lands on its row in Updates asking whether to raise it — pick a raise or type a number, and the automation picks its work back up within a couple of minutes; choose to wait and it resumes tomorrow. Two things are never lost to a pause: a one-time scheduled run and any message you send the automation — those run once the pause lifts. - A session that ignores the wind-down and crosses the ceiling anyway is simply stopped. That is not a failure: the automation stays active, nothing counts against its reliability, and it resumes the next day. 100,000 is also the most an automation can have. Name a lower number when you create the automation, or ask in chat any time ("cap the digest at 2,000 credits a day"), and that number is used instead. Sandbox sessions count toward the same ceiling. Automations don't expire on a timer — an automation lives until it concludes or you stop it. Budgets and the panel's per-automation cost display are what keep a forgotten automation from becoming a surprise. ## Scheduling Automations support three kinds of schedule: - **Recurring at a time of day** — "every morning at 9am", "Mondays at 8". Runs on a cron schedule in your local timezone, so it stays correct across daylight-saving changes. - **Recurring by interval** — "every 2 hours". Minimum interval is one hour. - **One-time** — "tonight at 11pm", "in three hours". Runs once, then the automation is done. Creating an automation never runs it. Whatever its instructions hold, nothing happens until its trigger fires, you press **Start** in its panel, or you ask for a run now. An automation with no schedule runs only on those last two. ### Starting now *and* staying scheduled A schedule doesn't have to mean waiting: for two-phase work — "message everyone today, then nudge stragglers each morning" — Avi arms the schedule (often with an event subscription too, so it reacts the moment each reply lands) and, because you asked for today's pass, starts that first run as a separate step with your words behind it. If you did not ask for a run now, the first run waits for the first tick, and you can say "kick it off now" at any time — it runs immediately without disturbing the schedule. ## Feed subscriptions Besides (or instead of) a schedule, an automation can subscribe to the project's [Updates feed](/docs/updates), so it runs the moment something newsworthy lands there — whatever the source (GitHub, email, Slack, AWS, an app): - "Triage every PR on my repo" — runs whenever a pull-request update appears or gains activity in the feed, with that update's headline and latest activity as its input. - "When anything new lands in the feed, summarize it to Slack" — runs on every new update. Because the trigger is the curated feed — not raw source events — the automation reacts to the same deduped, living stories you see in your feed. Subscriptions can be narrowed by source ("only GitHub activity"), by a specific thing ("only PRs from `acme/api`"), or by headline keywords, and combined with a schedule (a daily digest that also reacts to PRs). Housekeeping changes that don't add news never wake an automation. If updates land while sessions are in flight, they run side by side — unless the automation works one session at a time. Either way, everything that arrives while a session waits to start is handled together in that one session, so a burst costs one session, not one per update. The max daily spend keeps a noisy feed from running away. One thing to know: the feed is **curated**. An editor decides what's newsworthy enough to appear there, so something low-key — a brief personal note, a routine notification — may never become a feed update at all, and a feed-subscribed automation won't run for it. When the automation must catch a *specific occurrence every time*, it uses an event subscription instead. ## Event subscriptions A trigger names the app its event comes from ("CRM deal created", "Calendar event"); one that would fire on that event from every app is refused, except messages, which many apps deliver. Changing an automation's schedule or triggers never runs it, and nothing runs unless you asked. An automation can also subscribe directly to the project's **event log** — the raw record of everything Avi ingests (every email, chat message, calendar change, repo event, app event), before any curation: - "Whenever my grandmother emails me, draft a reply" — runs on every inbound email from her addresses, even a one-word hello. - "When Jane messages me, ping my phone" — runs per message from her handle, on any channel that carries it. - "When something lands in #deals, summarize it for me" — runs on posts in that chat channel. - "When a meeting with the Acme folks changes, re-check my prep doc" — runs on calendar activity involving those attendees. - "On every new support ticket, do a first-pass triage" — runs per ticket event, no editorial gate in between. Event subscriptions match on the event's *envelope*, and the filters compose: - **Type** — what kind of event, with wildcards: inbound email, a chat message, a calendar change, repo activity. - **From** — the sender's email address(es) or chat handle(s), or a bare **domain**. This is deliberately *per-sender, not per-thread*: replies get knocked out of threads all the time, but mail from `jane@acme.com` is mail from Jane wherever it lands. List every address a person writes from. For an automated sender — a school's notification system, a bank, a service's alerts — use its domain (`parentsquare.com`): those systems often mint a different `donotreply+…@` address for every message, and a domain matches all of them. A plus-tag never breaks a match either: `donotreply@x.com` matches `donotreply+abc@x.com`. - **Involving** — the person in *any* role: an email's to/cc, a calendar event's attendees. - **Channel** — the chat channel a message was posted in. - **Provider** — which service carried it ("avi", "slack", "google"). This matters for chat handles, which are only unique *within* a service: "jane" on Slack and "jane" on avi messages can be different people, so "when Jane messages me on Slack" pins both the handle and the provider. (Email addresses are globally unique, so they rarely need it.) They're exact and predictable: if the matching event lands, the automation runs; if it doesn't, nothing runs and nothing is spent. That's also why narrow filters matter — every match wakes the automation, and "every inbound email" includes the newsletters. When you name a person or a channel, the agent arms the filter to match just that; describe the trigger as precisely as you'd like the spend to be. Use event subscriptions whenever missing an occurrence isn't acceptable; use feed subscriptions when you want to react to the bigger, curated story. Bursts gather the same way, and an automation can never trigger itself: neither the platform's internal events nor the automation's own edits (a note it updates, a task it creates) will re-fire its own subscription. ## Relationship to Tasks [Tasks](/docs/tasks) are to-do items — they track work, they don't run it. Automations are the engine: when you ask for anything automated, the agent sets one up. ## Good uses - Daily briefings ("every morning at 7am, email me a summary of new GitHub issues"). - Monitoring ("every 15 minutes, check if our homepage is up; page me if not"). - Reacting to activity ("triage every PR from my repo and make a task if it needs my review"). - Coding work on a repo belongs to the Code app, not an automation. - Weekly reports. - "Work on this in the background" requests you explicitly want as their own automation. - Anything you'd otherwise have to remember to ask for again. --- # Agents An **agent** is a project your org owns. It has a name, a picture and a handle (`@concierge`), people who work with it, its own apps and accounts, and its own Instructions. Its [automations](/docs/background-agents) are its work. Where a personal project is yours alone, an agent is the org's: its creator is its first manager, it outlives any one person, and everyone on it sees the same work. Use one for work that belongs to the team rather than to a person: a customer concierge that looks after every customer, a support desk that answers `support@` and escalates to engineering, a recruiter that moves every candidate through the same steps. ## Making one Open the org menu — the org's square at the foot of the rail — and choose **New agent…**. Give it a name and, if you like, a picture (it wears a bot until it has one) and the people who should be on it. It opens on its Apps, where you connect what it will use — the team's shared mailbox, the org CRM, Drive — through the ordinary app flows, once. An account you connect to an agent is used by everyone who can talk to it, so connect the team's accounts there, never your own. The org's square at the foot of the rail is the switcher: your **Personal** project with your photo, then the agents you are on, then your orgs below a rule. Pick one to go there. While you are in an agent, its picture sits in the corner of the org's square. Write what it is for in its **Instructions**. Every automation in the agent follows them, so "change how we greet new customers" is one edit. The agent gets a handle the moment it exists — a starter; a manager can claim a custom one in its Settings, and change it later, as you can for your own (an old handle is retired, never reassigned). ## Sharing An agent has two roles: - **Members** open the agent, read everything in it — its automations, their sessions, its feed, its files, its Settings and Instructions — and talk to it. A member can run an automation and message it; a question it asks is answered by a manager, so a member's message never counts as the answer. Setting one up, or changing its instructions, is a manager's. - **Managers** also change it: its name, picture, people, apps, Instructions and automations. Org owners and admins manage every agent. A manager adds people from the agent's **Settings → Sharing**, and can open the agent to the whole org, which makes every org member a member. Anyone named on it can take themselves off (an org owner or admin still manages it afterwards). ## Talking to it Open the agent and type in **Chat**, or send `@concierge` a message in **Messages** (its **Agents** tab lists every agent you can use; pick one to open your conversation with it). Both are the same thread: your own, private conversation with the agent — nobody else on the agent sees it, not even its managers, and theirs stays theirs. Its reply comes back where you asked. Only the people on the agent can talk to it; a member can ask it anything, run its automations and message them, but cannot change it through chat — setting automations up, and its Instructions, are its managers' to change. The agent's work is shared; the conversation is not. ## Its work Automations in an agent work exactly as they do anywhere, with one difference in how you use them: when the work is one thing per customer, candidate or order, it is one automation per customer, in the agent — usually a [group](/docs/background-agents#groups), so they all share one setup you change in one place. Ask the agent, as a manager ("onboard Acme"), and it adds one; an automation that finds new customers — on a schedule, or when a company is created in the CRM — adds each one itself. Each runs on its own, with its own thread, triggers, stage and verdict. An agent runs up to ten sessions at once; the rest wait their turn and start as sessions end. Nothing fails or is skipped for waiting. An automation that works one session at a time still does. ## When it needs you When an automation in an agent needs a decision, it asks by message. A question for a particular person — "send this to Acme from your account", named by their handle in the automation's instructions — goes to them. Anything else goes to the people named as its managers (the org's owners, when none are named). In Messages the question shows as a **Needs you** card, the same one your Updates feed and the automation's Activity show: the question in full, **Approve** and **Decline** (with an optional reason) or the answer form with its choices, and what happens if nobody answers by the deadline. A review list (several things to approve or decline at once) shows every row, each approved or declined on its own, or all at once. The automation's name on the card opens it. You can still answer in words: reply with one of its choices; anything else is just a message to the agent. An open question — one without choices — takes any reply, as long as it is the last thing the agent said to you; once the agent has answered you since, your words are a message again, and the card is the way to answer. A send asked of you by name goes out from your own account; the agent's own sends go out from its accounts. When someone else answers first, the card says so, and you hear so in the same thread. The agent's own feed keeps the story of what happened; the asking lives in Messages. ## Credits An agent draws on the org's credits like every member. It is not a seat and is not billed as one. --- # Updates The Updates feed is your project's living timeline — what's happening across everything connected to the project, curated automatically so you can catch up at a glance. ## How the feed works Every row in the feed is a **topic**: one real piece of work, followed from start to finish. That can be a deal, a feature, an incident, an order, a hire, a meeting being arranged, or one of your own [tasks](/docs/tasks). Each topic is named for what it's about ("Sales: Acme — Enterprise, Q3", "Incident: checkout — error spike") and gathers everything about that work in one place, from every app you use. The same deal discussed over email, in chat, and in your CRM stays one topic. As new activity arrives, the topic gains a new **beat**: a one-line headline of what just happened, with detail underneath. The newest beat is the topic's current status. Click a row to open the topic's page: anything waiting on you at the top, then its whole history, newest first; **back** returns you to the feed where you were. Timestamps show when things actually happened (when the email arrived, when the message was sent), not when Avi processed them. - **Your tasks are topics.** When something happens that is the work of one of your open tasks, such as the vendor's quote for "Get the office quote" or the signed contract for "Send Hooli the contract", it lands on the task itself, titled by the task's name, instead of on a new topic beside it. A task never shows up on its own. It appears the first time real work about it arrives. After that, completing or reopening the task shows on the same topic, and renaming the task renames the topic. If a topic about that work already exists, it is folded into the task: its whole history moves under the task's name, and later activity lands on the task. Open the task from the topic with **Open task**. - **Many events, few rows.** A busy topic is still one row: it moves to the top when something worth a look happens, and routine progress is added to its timeline quietly without moving it. Recurring notices, like marketplace renewals, payouts, or sign-ups, are kept together as one topic per month with a running tally, not a row for each one. When a few messages about the same moment arrive together, they're written as one beat. - **Routine record activity is one row a month.** Records moving through your tools in the ordinary course of business — deals appearing and changing stage in your CRM, for example — are kept on one topic per month ("Sales: Pipeline deals — October") with a running count, however many records there are. A record gets a topic of its own when there is a reason for you to follow it: it is yours (the deal's owner is you), people are already emailing, messaging, or working a task about it (its changes then land on that conversation), or it turns — a deal won or lost always gets its own topic, worth everyone seeing. Older per-record topics that held nothing but routine activity are folded into the month's row. - The unread dot is personal: it tracks what *you* have and haven't seen. A topic you've read comes back as unread only when something worth a look happens. - The small icon on a row shows where its latest activity came from (email, Slack, and so on); a task's row shows a checkmark. - A topic that has gone quiet comes right back to the top when its work picks up again. - In the Avi mobile app, tabs at the top sort your [automations](/docs/background-agents) by what they're doing, under a one-line summary ("1 needs you, 2 working"). **All** is the feed. **Needs you** lists automations waiting on your answer — the number beside it is how many. **Active** lists every live automation: the ones working right now first (the number beside it counts those), then the ones waiting for their schedule or trigger, then any paused for the day. **Finished** lists what your automations concluded, succeeded and failed, newest first. Open an automation from any of these tabs to answer it or read its full outcome. Automations you stopped or archived live in the Automations panel. You can say the tab names in a voice session. - The feed stays current on its own as activity arrives. In the mobile app, you can also pull the feed down from the top to refresh it on demand. ## Automatic curation Avi curates the feed for every project by default — no setup. As activity arrives from your connected sources, Avi decides what's worth keeping, places it on the topic it belongs to (or on one of your tasks, or starts a new topic when it's genuinely new work), and writes the timeline beats. Replies in the same thread stay together on one update, so a conversation is understood in context rather than message-by-message. Avi keeps the **substance** from everyone in your conversations — decisions, agreements, action items, key facts, open questions — whoever said them, you included, so you and Avi share the full picture of what your team is doing. It filters the **noise** — newsletters, bulk mail, and content-free chatter like reactions, acknowledgements, and off-topic banter — no matter who sends it. A message that @mentions you is always given full consideration — and so are your own messages: Avi reads each one alongside the conversation it's replying to, so a quick "approved", "ship it", or "yes, go ahead" from you lands as the decision it is, not dismissed as a throwaway. Curation never creates or changes tasks. Tasks are yours. Avi only records what happened, on the task it was about. ### The Curator Every project has a **Curator**: the automation that keeps this feed. You'll find it in the project's [Automations](/docs/background-agents) list. It's always on — it can't be stopped, archived or deleted, and it needs no setup. - **It waits two minutes, then reads everything together.** When new activity arrives, the Curator starts two minutes later, so a burst of replies in one thread is curated as one development, not one message at a time. - **Some things are filed straight away.** An app asking for your approval, and changes to your own tasks, land on the feed without waiting for the Curator. - **It looks things up before it decides.** When a message can't be understood on its own — a quick "lgtm" to something it can't see — the Curator reads the thread, task, deal, event, pull request, ticket or file it refers to first. - **Tell it what matters.** Its **Instructions** tab has two views, picked from the menu at the top: **Built-in** (Avi's curation rules, kept up to date; you can read them but not change them) and **Yours**. Write what matters in this project's feed, in your words — yours are added to the built-in ones, and where the two disagree, yours win. You can also ask Avi in chat ("tell the Curator customer escalations always matter"). A few things skip the editor entirely because you must never miss them: an [automation](/docs/background-agents) concluding its work, hitting its daily budget, or **asking you a question** always posts directly. These items are titled by the automation ("*Collecting shirt sizes from the team*"). When an automation is, or was, working a topic, the row shows it on its own line under the latest activity: the automation's name and its state — a live state (working, needs you, paused) as a badge, a finished one as plain text with when it ran — so a completed automation never reads as the outcome of later activity on that topic. A question shows as a **Needs you** item — open it and answer right in the feed (tap a choice or type a reply), and the automation resumes with your answer. Each question carries a deadline and the automation's stated fallback, and its row keeps the whole story: asked, then answered — or expired into the fallback. Some apps also publish their own digest updates straight into the feed. Those belong to the app that writes them: curation never merges, regroups, or rewrites a digest. ## Grouping is automatic Keeping related activity together is Avi's job, not yours. The Curator places each development on the topic it belongs to — on one of your tasks first, when it's that task's work — and opens a new topic only for genuinely new work. Two of your tasks are never merged, and a topic that turns out to be one of your tasks' work is folded into the task. ## Beats know their records Opening an update shows its whole story, newest beat first, every beat in full — nothing to expand. Every beat Avi writes remembers exactly which records it came from — the email thread, the deal, the file, the ticket — and the app's own link to each. Each beat carries those links to jump straight into the source app, and when you ask Avi to act on something in the feed ("reply to that thread", "open that file"), it goes directly to the record instead of searching for it first. ## Ask Avi about your feed When you chat with Avi in a project, it already has your most recent updates in view — so you can ask "what happened today?" or "anything I missed on the Acme deal?" and get a straight answer without telling it to go look. For anything older or more specific, it pulls the full feed or opens a single update to read the whole timeline. --- # Apps An **app** is a bundle of related tools the agent can use — and often more: a panel of its own, background automation, activity feeding your project's Updates. Apps are turned on **per project** — so your engineering project might have the Code app on, while your sales project has the CRM on. You control what the agent has access to, project by project. Seven apps are built in and **always on**: Drive, Email, Messages, Calendar, [Tasks](/docs/tasks), [Contacts](/docs/contacts) and the [Wiki](/docs/wiki). They're in every project from day one, with their own panels and nothing to enable, and they can't be switched off. Everything else in this guide is opt-in, per project. ## Finding apps A project's settings → Apps has two tabs: **Installed** — the apps already on in this project — and **Discover** — everything available to add: apps published by Avi, public apps from other publishers, and any apps your own team has built and deployed. Searching looks across both at once. Apps published by Avi include: | App | What it does | |---|---| | **AWS** | Read anything in a connected AWS account, plus background monitoring — error spikes (including your own custom error metrics), firing CloudWatch alarms, security findings, and critical changes surfaced into the project feed. | | **Code** | Your GitHub, inside Avi. Connected accounts' repos, pull requests, issues, code files, and search — browsable in the panel (fully voice-operable) and readable by the agent, with PR reviews, merges, and issue writes from chat — and meaningful repository activity (merged pull requests, releases, issues) feeding the project's Updates. Connect GitHub once and it all just works. Set up a **Factory** on one or more repos in the Code panel and it turns issues into reviewed pull requests, each built in its own Coding Sandbox. | | **CRM** | Your organization's book of business — companies, contacts, pipelines with stages, deals, and an activity log. Works standalone (Avi is the system of record, shared across every project in the org), or connected to your own CRM (HubSpot today): everything is read from and written straight to your CRM, so what you see is always what it holds — nothing is copied or synced. Includes a panel for deals, contacts and companies. | | **Freshdesk** | Support tickets — list, create, and update them, with new and updated tickets surfaced into the project feed. A reply to a customer goes out only once you OK it. | | **Drive** | Always on. Your files, documents, and notes in one place — one tree, searchable end to end, with Google Drive and ShareFile mounting in as roots when connected. | | **Email** | Always on. A real inbox on the project's own avi-mail.com address out of the box, or switched to your connected Gmail — one inbox per source, reviewable drafts, and spam that never sneaks in. | | **Messages** | Always on. Chat with other orgs on the project's own @handle out of the box, blended live with a connected Slack workspace — one inbox, channels and DMs across both. | | **Calendar** | Always on. Your calendar and scheduling, standalone or on Google Calendar. View and manage events (multiple named calendars with colors, recurrence, attendees, all-day spans) with a draft-first review loop and an agenda panel; bind a Google account to work live against Google Calendar instead of the built-in store. Plus the scheduling layer: shareable video-meeting links (a connected Zoom account mints real Zoom meetings, no calendar event required) and a public booking page — anyone picks a time from your real availability, confirmed bookings land on the active calendar with the visitor invited, and you choose whether booked meetings carry a Google Meet or a Zoom link. | | **Resend** | Marketing email broadcasts through your Resend account. Ask the agent to draft a broadcast — the email template, subject, and audience segment — then review it in the Resend panel: see exactly what will go out and send it yourself with one click. Nothing sends without you. Delivery and engagement analytics live in Resend's own dashboard. | | **Sentry** | Your Sentry errors, inside Avi. Browse and filter issues in the panel, read the latest stack trace, breadcrumbs, the distributed trace it happened in, and tag breakdown, and resolve, archive, assign, or comment, by hand or by asking Avi. Avi can also search spans and read whole traces to answer performance questions. New issues are checked every 15 minutes and show up in the project feed. | | **Social** | Your social presence in one place — X today, more networks to come. Connect one or several accounts; follow mentions, lists, searches and timelines on a voice-driven board; read mentions and replies; draft posts that Avi publishes only when you say so; and get briefings on what your board is following. | ### The Social app Connect your X account from the Social app's settings — one click through X's consent screen, nothing else to set up (the platform provides the X API access and covers its usage). Connect several accounts if you speak from more than one. If you connected before list columns existed, reconnect once to grant list access. Once connected: - **The board.** A deck of live columns, one feed each: your mentions, your home timeline, one of your lists, a search on a topic, or a user. Add as many as you like, reorder and resize them from the column menu, and sort any column by latest or top (top ranks the loaded posts by likes, reposts and replies; a search column asks X for its most relevant posts). Every column has a filter (original posts, replies, reposts, quotes, media, links) you can drive by voice, and leaves out posts from accounts that look like bots or spam unless you ask to include them: *"show filter"*, *"only replies"*, *"no reposts"*, *"clear the filters"*. Removing a column always confirms first. Posts read like they do on X, with photos, videos, quoted posts, X Articles, link previews and the reply, repost, like, views and share icons; like or repost straight from the feed, and links to X posts, articles and profiles open right in the app. Opening a post shows the posts it answers above it and its replies below as conversations (for posts from the last seven days), top replies first; open any reply to follow it further. Everything works by voice, instantly: *"scroll economics down"*, *"scroll the second column to the top"*, *"select economics"* then *"scroll down"*, *"make wider"*, *"move left"*, *"sort by top"*, *"refresh column"*, *"next column"*, *"add a column"*, *"pause economics"*, *"open the third post"*. - **Your activity flows into the feed.** New mentions and replies on your threads land in the Activity tab and in the project's Updates, grouped with related email and chat activity about the same matter. - **Briefings from your board.** Every search, list, mentions and home column on the board is checked about once an hour and, when something noteworthy happened, a short briefing lands in your feed: the themes, the notable voices, the posts worth reading. Nothing to set up — add a column to get its briefings, remove it to stop. Quiet columns are checked less often until they pick up again. - **Posting costs are always surfaced.** X charges much more for posts containing links ($0.20 vs about $0.015) — Avi will always tell you and ask before sending one. - **Drafts.** Ask Avi to draft a post or a whole thread and it lands in the Social app's **Drafts** tab — with a preview that looks exactly like it will on the network: threads, reply context, photos, GIFs and videos (from Drive or a link, on any post of a thread), character counts. Avi links you straight to any draft it creates; when you're happy with one, ask Avi to post it, photos and videos included. - **Avi never posts on its own.** Posting is refused outright for any background or automated caller — publishing works only when you explicitly ask in chat. ## Building your own You can build and deploy your own apps — an internal API, a vendor Avi doesn't cover yet, a custom panel — with the [Avi CLI](/docs/apps). Your team's deployed apps show up in project settings next to Avi's, and enable the same way. ## Reading an app's documentation Every app ships its own documentation, and it updates automatically with every new version of the app — so what you read always matches what's deployed. Each app has an **intro page**: an overview written by the app's author, framed by a generated header (name, one-line summary, version, last updated) and a generated reference of every tool, setting, webhook, and data store the app ships. Deeper pages cover things like how its integrations authenticate, what runs on a schedule, and troubleshooting. You'll find it in three places: - **Discover** — open any app from the Discover tab to read its full intro page before enabling it. - **The app's Docs tab** — for apps your org built, the org settings app detail has a Docs tab with every page. - **The agent** — project agents read the same docs, so you can just ask ("how does the Calendar app decide what to sync?") and the agent consults the app's documentation before answering. ## Enabling an app In a project's settings → Apps, press **Enable** on the apps you want. Enabling an app: - **Turns on all of its tools at once** — the agent can use the whole app immediately, with no need to enable each tool by hand. (You can still turn individual tools off afterward — see below.) - May ask you to approve the app's **capabilities** — what the app is allowed to read and write. If a later update to the app requests new capabilities, the project re-approves them before the app runs again. Apps made by Avi, like Drive, Email and Calendar, never ask: they come with the access they need, including after updates. - May add its own panel to the project, opened from the rail. - May require you to connect an account (e.g. linking GitHub or HubSpot via OAuth). - If the app does background work on a schedule, enabling it kicks off a first run right away — you don't wait for the next scheduled tick. Each project remembers which panel you had open — and where you were inside it. Switch projects and come back, reload, or share the URL: the panel reopens exactly where you left it (the open panel and its view live in the page URL). ## Connecting an account Some apps run on an outside service — the Email, Calendar, and Drive apps on a Google account, Messages on Slack, CRM on HubSpot, Code on GitHub, Social on X. You connect that service from the app itself: its settings screen shows the account as a row with a **Connect Google** (or whichever service) button: pick what to share, sign in at the service, and you land straight back on the app's settings screen. One sign-in sets up every app that uses it. The consent screen lists what the service can share, and each row names the apps that will be set up with it — connect Google from Email with Calendar and Drive ticked, and all three are configured the moment you return. Connect Drive later and the same Google account simply widens. Each connected account shows up in every app that uses it, with **Also used by** naming the others. **Use here** puts it to work in this app, and **Stop using** takes it out of this one (that app goes back to running on its own). The row's **⋮** menu holds the rest: **Reconnect** goes straight to the service's sign-in with the access the account already has, and every app using the account picks it up at once; **Permissions** changes what the account shares; **Disconnect** removes it from the project and every app bound to it. If an account loses access — you changed your password, or removed Avi's access at the service — every app that uses it shows a **lost access** strip across its top with a **Reconnect** button. Reconnect once, from any of them, and they all come back. Connected accounts only ever reach Avi's own apps. An app from any other publisher connects to its services with its own credentials — its settings screen says so instead of offering your accounts. Beyond connections, an app's settings screen holds any other options the app offers. For example, the Email app lets you toggle the small "(Email by Avi)" signature it appends to emails it sends and drafts — it's on by default, and you can switch it off there. Some apps ask for a credential that isn't an OAuth connection — an API key or token you hold. Those settings are a dropdown over the project's **Secrets**: add the key under the project's Secrets tab first, then pick it in the app's settings. The app only ever stores a *reference* to your secret — it can't read your other secrets, and the actual value stays protected by Avi and is handed to the app securely each time it runs. If the dropdown is empty, add a secret first. ## Tool-level control Enabling an app switches all of its tools on for you. If that's more than you want, you can disable individual tools inside it — useful if you want the agent to *read* from a service but not *write* or *delete*. Use the **Tools** tab in the project's settings. ## Webhooks Some apps receive events from an outside service in real time (for example, a support desk pushing new tickets). When an app does, its settings page shows a **Webhooks** section with one entry per webhook: - **URL** — a unique, secret address for this project. Copy it into the outside service's webhook settings. Because the URL is secret, treat it like a password. - **Rotate** — generates a fresh URL and immediately disables the old one. Use this if a URL might have leaked; then update the outside service with the new URL. - **Enable / Disable** — Disable is an instant off-switch: deliveries to that URL stop being accepted. - **Secrets** — some apps ask you to bind a vendor secret (e.g. a "signing secret" the service gives you) so the app can verify each delivery is genuine. Pick the matching **project secret** from the dropdown. If it isn't listed yet, add it first under the project's Secrets, then come back and select it. Deliveries only flow when the app is enabled and the webhook is active. ## What apps the agent can actually use For a tool to be callable, two things must be true: 1. The app it belongs to is enabled in this project. 2. The specific tool is permitted in this project. Both gates matter. Turning off a single tool is a quick way to keep the rest of an app's capabilities while blocking one risky one. --- # Files Every project can have files the agent can read, write, and edit. They're stored in Avi's cloud storage, scoped to your project. The agent can: - Search and list files across the project — one federated search that also covers a connected Google Drive or ShareFile account. - Read any file. Text files (markdown, CSV, JSON, code, etc.) come through as text. **Images are seen directly, and PDFs are read on any model — their text, or the page itself when it is a scan.** Word, Excel, and PowerPoint files (.docx/.xlsx/.pptx) are read as extracted text — spreadsheets come through sheet-by-sheet so Avi can reason over the rows — and when layout matters (a table, a slide, a chart), Avi can look at the file as rendered pages instead. Avi can also check a file's size, checksum, and an image's pixel dimensions without reading it. Avi can also change the wording of a Word document for you, in place, with its formatting preserved. - Write new files (text, or binary from generated bytes) and download files from the web by URL. - Edit parts of existing text files in place. - Create notes, make business documents — quotes, invoices, engagement letters and more — as Word documents from your own templates in Avi's document design (working out every amount and total itself; a detail it doesn't have — your address, a phone number — is left out rather than left blank, and added when you give it; the typeface travels inside the file, so it looks right for everyone), and produce new files as it works — exported PDFs, converted or resized images, Office files exported to PDF or converted between formats (an old `.doc` to `.docx`, a spreadsheet to CSV). - Fill out fillable PDF forms, accurately and completely (see [PDFs](#pdfs)). - Move and rename files or whole folders (moving a file under the Google Drive or ShareFile root uploads it there). - Delete files (recoverable for about a week — ask Avi to undo a delete, or restore from Drive's Recently deleted). - Create and change Excel workbooks: values and formulas, rows and columns, formats, sheets, tables, dropdowns, highlighting rules, charts and summaries, with every formula recalculated (see [Excel workbooks](#excel-workbooks)). - Write and format Word documents with real styles, numbered clauses, lists, tables, and page numbers; redline them with tracked changes in your name (and a clean copy that carries no editing history); comment, reply, and resolve; compare two versions; and export to PDF, PDF/A, A4, or one section. All of this happens through the **Drive** app's tools — files (Word documents included) and notes are one tree with one set of tools. ### Getting files in There are four ways to put a file into a project: - **Drive panel**: open the Drive app from the rail and drag files in (or pick from your computer). They appear in the tree and are immediately readable by the agent. - **Paste or drop in chat**: paste a screenshot from the clipboard, or drag a file onto the chat composer. Attachments are saved into the project's **`Attachments/` folder** automatically, so they show up in Drive and Avi can come back to them in later conversations. Limits: 8 attachments per message, 10 MB each, 20 MB in all. Very large images are automatically downscaled before they're shown to the model (the stored file keeps its full resolution). - **Let the agent create them**: Avi can write new files as it works (a generated report, a chart, a transformed CSV). - **Pull them in from connected apps**: with the Email and Drive apps connected to your Google account, ask Avi to save an email attachment, a Google Drive file, or a ShareFile file into the project — "grab the invoice PDF from Dana's email and review it". The file lands in the project's files (email attachments default to the `Attachments/Email/` folder), and since PDFs and images are read natively, Avi can review the contents right away. Email attachments up to 10 MB; Google Drive and ShareFile files up to 32 MB. Files persist for as long as the project exists. ### Getting files out Project files also travel the other way. With the Email and Drive apps, Avi can **attach project files to emails** it sends or drafts ("send the Q3 report to Dana as an attachment" — up to 10 files and 18 MB of attachments per email) and **move project files to Google Drive or ShareFile** byte-exact, including binary files like PDFs and images; the project copy is removed once the upload lands. ### Editing files safely When Avi edits part of a file in place, it has to point at a snippet that's unique in the file — otherwise the edit might land at the wrong copy of a common phrase. If your file has the same exact line in two spots, Avi will ask for more context to disambiguate, or you can tell it explicitly to replace every occurrence. Edits and overwrites are also protected against collisions: if a file changed between Avi reading it and writing it back (say, a teammate re-uploaded it), the write is refused and Avi re-reads before retrying — so nobody's changes get silently overwritten. **A file you have open follows a rename.** If Avi (or you, in another tab) renames or moves the file on screen, the view moves with it and says where it went — it never closes as if the file were gone. **Nothing is lost when a file is replaced.** Whenever Avi overwrites, edits, or replaces one of your files, the previous version is kept in a hidden working area for about 7 days (the twenty most recent versions of a file that changes a lot) — open **Working files** from the Drive panel's root menu to see it, and ask Avi to restore it if a change should be undone. Avi also uses the same area to stage things it produces (a converted image, a filled PDF) so it can double-check the result before placing it in your tree; anything left unplaced cleans itself up after a week. **Notes get the same protection.** Avi can't replace a note's whole body without having just read it — a blind rewrite is refused outright, and targeted edits only ever change the exact passage they name. Whenever a note's content does change, the previous body is kept in the same working area for about 7 days, so any edit can be undone by asking Avi to restore it. ### Word documents Avi works on Word documents (.docx) itself, right in the conversation — there is no separate agent to hand the work to, and the answer comes back in the same reply. When a request is about a document (you mention one, attach one, or have one open in Drive), Avi brings its Word know-how to it automatically. - **Everyday changes.** "Change 30 days to 45", "add our address to the letterhead", "make the headings dark green", "the pricing table should be one row and a total" — done in place, in seconds, with the formatting kept. - **Rearranging.** "Delete the second paragraph under Terms", "put these four lines in two columns", "move the client details to the top right", "move Terms above Pricing", "turn this list into a table" — paragraphs and tables can be removed, moved, and rebuilt; lines that are only being moved keep their formatting exactly. - **Business documents from your templates.** Quotes, estimates, engagement letters, statements of work, invoices, one-pagers, and cover letters, made from your own template (your own template for a document type is what Avi starts from — the built-in design only when you have none; or whichever you mark as the default for that type) in your `Documents` folder. Avi fills in every detail it knows, writes the line items, and works out every amount, tax, and total itself. Templates are covered too: making one, changing its wording, giving it a logo, making it the default. - **Tracked changes.** "Redline", "mark up", or "propose" means tracked changes on the document itself, one per change you asked for, credited to you. Avi never rebuilds the document or renumbers clauses you did not mention. Templates are changed directly, never with tracked changes. - **Comments**: adding, replying, resolving. - **New documents** with real headings, numbered clauses and numbered lists. - **Accepting or rejecting** changes, and producing a clean copy with the editing history scrubbed. - **Comparing two versions** into a redline, when you give it both. - **It looks at the pages.** After making a document or changing its layout — a new row, a signature block, a moved section — Avi looks at pictures of the pages the change landed on before answering: that the change is there, that it matches its neighbours, that nothing was pushed onto a new page or cut in two. If something is off it fixes it once more and looks again; if it still is, it tells you exactly what. - **PDFs only when you ask.** A document is done as a Word file you open in Drive; it exports to PDF (clean, archival PDF/A, A4, one section, with bookmarks) when you say so, never on its own. When you change a document that has a PDF of it beside it, Avi tells you that PDF still shows the old version. It works the way a document professional does: one tracked change per edit, never accepting its own changes, never touching the other side's changes or comments unless you say so, and a clean copy only when you ask. When an edit moves clause numbers or touches a defined term, it lists the cross-references for you to decide on rather than changing them silently. When something can't be done with a Word document yet (a footnote, an image in the text, merged table cells), Avi says so plainly instead of trying things out on your file. ### Excel workbooks Avi works on Excel workbooks (.xlsx) itself, right in the conversation. When a request is about a spreadsheet (you mention one, attach one or a CSV, or have one open in Drive), Avi brings its Excel know-how to it automatically. - **New workbooks.** "Make a budget tracker for the first half of the year", "build a price list with a margin column", "turn this CSV into a spreadsheet with totals" — a real workbook with a header row, number formats, and formulas for everything that is calculated. - **Everyday changes.** "March rent went up to 2,400", "add these three orders", "add a tax column at 8%", "sort by amount, largest first" — done in place. New rows go above the totals row and the totals grow to cover them. - **Formulas, not typed-in numbers.** Totals, margins, lookups and summaries are written as formulas, so they keep working when you change the data. Avi never types a number over a formula without telling you. Ask for a calculated number to come out at a target ("make the total 250,000") and Avi says what it is calculated from and asks which numbers to change before changing any. - **Always recalculated.** Every change is recalculated straight away, and Avi reports the numbers the formulas now show. A change that would leave a new error such as `#REF!` or `#DIV/0!` is not saved; Avi fixes it or tells you why. - **Rows, columns and sheets.** Insert or delete rows and columns, rename, add, copy, hide or delete sheets — every formula, chart and named range that pointed at the moved cells follows. - **Summaries.** "Revenue and order count by region on a summary sheet" — built with live formulas, and a chart if you want one. - **Formatting and rules.** Currency, percent and date formats, column widths, frozen headers, filters, tables, dropdown lists, and highlighting rules ("flag anything overdue in red") that stay true as the data changes. - **Charts.** Column, bar, line, area, pie, doughnut and scatter charts, as real Excel charts that follow their data. - **Questions.** "Which month had the best margin?" — answered from the cells, naming where the answer was read. - **A real spreadsheet view.** In Drive a workbook opens as a grid with sheet tabs and a formula bar. Click a cell, type, press Enter — it saves and recalculates. Select cells and ask Avi about "these". Highlighting rules show in the grid, including ones that light a whole row or match text. Charts, data bars and top-10-style rules aren't drawn in the grid; Export to PDF (in the ⋮ menu) shows charts. - **Everything else is left alone.** Charts, pivot tables, macros and pictures already in your workbook are kept exactly as they were. A few things Avi can't do in a workbook yet, and says so rather than trying things out on your file: creating or changing pivot tables (it builds the same summary with formulas instead), changing an existing chart's design, editing macros, and opening password-protected workbooks. Older `.xls` files are converted to `.xlsx` first. Excel recalculates everything again when you open the file, so its numbers are always the final word. ### PDFs Avi reads PDFs, fills their forms, writes on their pages, and signs them — and shows you a picture of what it wrote before calling it done. - **Reading.** Ask what a PDF says, or anything about it — a schedule, a contract clause, an invoice total. Avi reads the text of the pages it needs, on any model. A scanned page has no text to read, so Avi looks at the page itself, the way you would. - **Any PDF you throw at it.** Files locked against editing read like any other. A file that needs a password to open: give Avi the password and it reads it. A damaged file (cut short, exported badly) is repaired and read. Sideways and cropped pages are handled. - **Filling forms.** Avi reads the form's real field list — each field with the words printed beside it — so every value goes in the right field, even when the fields have cryptic internal names. A fill is all or nothing: a value that doesn't fit its field (a choice that isn't one of the options, text longer than the field allows) stops the fill, and Avi fixes it first. Every value is read back from the saved file, and Avi tells you which fields are still empty. - **Writing anywhere.** On a flat or scanned form — no real fields — Avi writes straight onto the page: a name in the blank after "Name:", a date beside a signature, an X in a printed checkbox, an image. It places things against the words printed on the page, so they land on their lines. - **Signing.** "Sign this for me" puts your signature on the document's signature line: your name in a handwriting style, or your own signature image if you've saved one in Drive (a PNG with a transparent background works best). When a document has more than one signature line, Avi asks whose is whose rather than guessing. This is a visual signature — how a signed page looks — not a certificate-based digital signature. Avi only ever signs as you, and only when you ask: asked to sign for someone else, it declines and offers to send them the document to sign themselves. - **Checked before it's done.** Avi won't write over existing words or off the edge of a page, and every write is verified by comparing the page before and after. Then Avi looks at a picture of each place it wrote, and fixes anything that sits wrong before telling you it's finished. - **Your original stays safe.** Changes are made on a working copy. Avi saves the result beside the original under a clear name — a filled form as "… (filled).pdf", so the blank stays blank — or replaces the original when that's what you asked for (the previous version is kept for about a week). - **Your call.** A form the publisher marked against changes, or a document that already carries a digital signature, is only changed after you say so — a change removes the protection or invalidates the signature. - **Pages.** Merge PDFs, pull out or reorder pages, rotate a sideways scan. The originals are never modified. - **International text.** Names in Cyrillic, Greek, or extended Latin alphabets (ł, ő, đ…) are written correctly. Not yet: turning a long scan into searchable text, changing words already printed in a PDF, redacting (a box drawn over text leaves the text in the file, so Avi won't pass one off as a redaction — it suggests removing it from the source document instead), certificate-based digital signatures, and Adobe "LiveCycle" (XFA) forms. Avi says so when it meets one. ### Getting documents signed by others Connect a ShareFile e-signature account (RightSignature; Drive → settings → E-signature) and Avi sends documents out for other people to sign, straight from the conversation. - **Ask for it.** "Send the Globex agreement to Dana at Globex and Casey to sign." Avi finds the signature lines on the document, marks where each person signs (and dates), and shows you the marked page — each box named for its signer — with a link to the file, before anything goes out. A Word document works too — it is sent as a PDF; your original is never changed. - **You say send.** Nothing is emailed until you've seen who signs where and said so. RightSignature then emails each person their own signing link. - **Follow it.** "Has everyone signed?" tells you where it stands, read from RightSignature. "Remind them" nudges whoever hasn't signed. "Cancel it" voids the request. - **It comes back on its own.** When everyone has signed, the signed copy and the signature certificate are saved beside the original as "… (signed).pdf" and "… (signature certificate).pdf". - **When Avi can't find the place.** It asks you where each person signs, or hands you RightSignature's own screen to place the fields on that document — Avi still tracks it from there. This is different from "sign this for me", which puts *your* signature on a page as a picture. Signatures from other people go through RightSignature, with their emails, links and certificate. Only RightSignature is supported today, and its API needs a ShareFile plan that includes e-signature. ### Finding files Avi's project-wide search covers files too — by name, and by content for text files. "Find the Q3 budget spreadsheet" works even when the filename doesn't say Q3. ### Large files Avi can read very large text files (logs, big CSVs) in pages — by byte range — rather than swallowing the whole thing at once. You don't have to think about this — Avi will page through automatically when a file is big. ### Browsing files Your project's files live in the **Drive** app alongside your notes, document templates, and connected cloud storage — one tree for everything. Select text in an open note or Word document and Avi knows what "this" means: ask "rewrite this" or "what does this clause say" and it works on exactly the highlighted part. Your files are yours until you share them: a file, a folder, or a document template can be shared with specific people or with everyone in your org, to view or to edit (**Share…** in its menu). What others share with you is in **Shared with me** at the top of your Drive. Open Drive from the rail to browse folders, preview files inline (images, PDFs, and Word documents render right in the panel — and a `.docx` can be edited and saved in place), download, upload by drag-and-drop, and manage folders. For the full tour, open Drive in the project's settings → Apps to read its own docs, or just ask Avi. The `Attachments/` folder is special: chat uploads land there and messages reference those paths permanently, so it can't be renamed or deleted. ### Custom metadata on files Every file carries a freeform `metadata` JSON object ([Data Types](/docs/data-types)). Use it to attach cross-system identifiers (`stripe.invoice_id`, `hubspot.attachment_id`), workflow state (`reviewed_by`, `team_owner`), or any tag you'll later want to filter on. Two systems writing different top-level keys never clobber each other. You can read or write a file's metadata from the agent (Avi sets keys as part of a file write, patches them alone, sees each file's bag in listings, and filters listings by metadata containment), or from apps. See the [Data Types](/docs/data-types) page for the full merge semantics and query shape. When you list a directory with a metadata filter, **directories with no matching files inside are hidden** — the filtered view is focused, not a tree of empty folders. List without a filter to see the full structure. --- # Data Types Avi is built on a small set of native data types that every project has from day one. The agent reads and writes them, apps extend them, and you can query them — they're the shared substrate the whole platform sits on. ## The native types - **Events** — an append-only log of everything that happens in a project: messages received, automation runs, deploys, app activity. Events are immutable and carry a structured payload. Apps publish them, and Avi's curation engine reads the log to build the [Updates](/docs/updates) feed. - **Files** — documents, images, PDFs, and data stored in the project. The agent reads them (PDFs and images natively), writes and edits them, and searches across them. See [Files](/docs/files). Three more types were once built into every project and now live in **apps**, each owned by the app for its domain — the records, panels and tools carried over unchanged: - **Tasks** — to-dos you and the agent track together: a name, notes, a priority (1–4, higher is more important), and subtasks. In the always-on **Tasks** app; see [Tasks](/docs/tasks). (For work that *runs* on a schedule, see [Automations](/docs/background-agents).) - **Contacts** — the people this project deals with: names, emails, phones, companies, tags, and notes, standalone or synced with Google Contacts. In the always-on **Contacts** app; see [Contacts](/docs/contacts). - **Notes** — free-form documents like meeting notes, runbooks, plans, and drafts. In the **Drive** app, alongside your documents and connected cloud storage. Everything that existed before each move was imported automatically, and old links open in the owning app. ## Custom metadata **Files** also carry a `metadata` field: a freeform key-value bag where apps, integrations, and your own code can store arbitrary data alongside each file. This is how Avi stays the most extensible AI assistant for real work: every file can be tagged, joined, and queried by the systems your team already uses — Stripe, HubSpot, Linear, Intercom, anything — without modifying the schema. ### What `metadata` is A JSON object on every File. The shape is open — you decide what goes in it. ```json { "stripe.invoice_id": "in_NffrFeUfNV2Hib", "hubspot.deal_id": "1234567", "team_owner": "alex@acme.com" } ``` There is no schema. There's no registry. You just write the keys you need. ### Convention: prefix your keys The platform doesn't enforce anything, but the universal convention is to **prefix your keys with a namespace** so two systems writing to the same file don't accidentally collide: - `stripe.customer_id` - `hubspot.deal_id` - `internal.priority_score` Pick something obvious (your tool name, your team) and stick with it. ### Writing metadata Every file write accepts an optional `metadata` field. From an app (with the `project:files:write` capability): **On create**, you supply the initial object: ```ts await context.projectFiles.put({ path: "invoices/2026-q1.pdf", data: pdfBase64, encoding: "base64", metadata: { "stripe.invoice_id": "in_NffrFe…" }, }); ``` **On update**, `metadata` is a **shallow merge patch**, not a replace: - Each top-level key you supply REPLACES the value at that key. - A `null` value DELETES that key. - Keys you don't mention are left untouched. - `null` ANYWHERE in the patch is stripped — including nested. `{ stripe: { token: null } }` stores as `{ stripe: {} }`. If you need to keep a `null` somewhere nested, just don't write it; store an explicit sentinel value instead. ```ts // Adds stripe.invoice_id without disturbing anything else await context.projectFiles.updateMetadata({ path, patch: { "stripe.invoice_id": "in_NffrFe…" } }); // Removes a key await context.projectFiles.updateMetadata({ path, patch: { "stripe.invoice_id": null } }); ``` This is the important property: **two apps writing different top-level keys never clobber each other**. Stripe's tool can write `stripe.*` while HubSpot's tool writes `hubspot.*`, and neither has to know the other exists. Files keep metadata beside the file, not in its bytes — so renames, moves, and re-uploads all preserve it. An app can set metadata alongside a write or patch it on its own, and the agent can do both when you ask. On re-upload of the same path, the new metadata is merged into whatever was already there. ### Querying metadata Listing files accepts a `metadata` filter. It matches files whose metadata **contains** all the key/value pairs you supply (JSON containment). ```ts // All files a review app has signed off on await context.projectFiles.list({ metadata: { "review.approved": true } }); ``` The matching is exact at each key — pass the value you stored. Nested objects compare by structural containment, so `{ stripe: { customer_id: "cus_…" } }` matches a file that has at least that nested shape. ### When to use it - **Cross-system identifiers.** The single most common use: storing the id another system uses for the same thing, so you can round-trip between Avi and that system. - **App-defined fields.** An app wants to attach a `priority_score` or `triage_status` without needing a new column. - **Ad-hoc tags or hints** the agent or your apps will later filter on. ### When not to use it - **Real first-class fields.** If a value belongs on every record of a type, it belongs in an app's own data, not in file metadata. Metadata is for the long tail, not the spine. - **Anything secret.** Metadata is plaintext and visible to anyone who can read the file. Secrets belong in **Secrets**, not metadata. ### Where it works Metadata as described here is a Files feature. Events and Updates aren't part of this surface: events are immutable log entries that carry their own structured payload rather than an editable metadata bag. --- # AI Models Avi ships its own catalog of top-tier models. You don't bring your own API key — Avi is the provider, and usage consumes credits at each model's own rate. See [Billing](/docs/billing). ## Catalog | Model | Best for | |---|---| | **Claude Sonnet 5** | The default. Balanced reasoning and speed. Works well for almost everything. | | **Claude Sonnet 5.5** | The newest Sonnet: an upgrade over Sonnet 5 at the same credit rate, and faster. Balanced reasoning and speed. | | **Claude Opus 5.5** | Hardest problems. Long-running coding and knowledge work, complex reasoning, high stakes; reports back clearly on what it did. Uses more credits per token. | | **Claude Fable 5.1** | For the hardest, highest-stakes work; among the priciest per token. Needs your org's data-retention opt-in first (see [below](#claude-fable-51-and-data-retention)). | | **Claude Haiku 5.5** | Fast, cheap, light tasks. Good for quick lookups or high-volume work. | | **OpenAI GPT-5.6 Sol** | OpenAI's flagship for ambitious agentic work. | | **OpenAI GPT-5.6 Terra** | Balanced OpenAI model for efficient, high-volume work. | | **OpenAI GPT-5.6 Luna** | Fast, affordable OpenAI model for everyday work. | | **OpenAI GPT-6 Astra** | OpenAI's most capable model. The hardest end-to-end reasoning, coding, and research work. Among the priciest per token. | | **OpenAI GPT-6 Sol** | OpenAI's daily model for complex coding and agentic workflows. GPT-6 intelligence at a fraction of Astra's price. | | **OpenAI GPT-6.1 Sol** | The upgraded GPT-6 Sol: close to Astra on coding, computer use, and professional work at about a fifth of Astra's price. | | **OpenAI GPT-6 Luna** | OpenAI's most efficient GPT-6. Summarization, extraction, classification, routing — high volume, very cheap. | | **GLM 5.3 Flash** | Frontier open-weight reasoning at a fraction of the cost. Fast, very cheap. | | **GLM 5.3** | Z.ai's open-weight flagship. Stronger reasoning than Flash, still affordable. | | **GLM 5.3 Fast** | GLM 5.3 on dedicated capacity for higher sustained throughput. Same reasoning, faster. | | **DeepSeek Flash 4.1** | DeepSeek's newer Flash generation. Cheap, fast, for high-volume everyday work. | | **Kimi K3** | Moonshot's open-weight flagship. Deep reasoning and long-horizon agentic work. | ## Picking a model - **Start with Sonnet.** It's the right call most of the time. - **Upgrade to Opus** when a task is hard, multi-step, or the stakes are high enough that you'd want a second opinion. - **Drop to Haiku** when you want fast and cheap — short summarizations, quick lookups. - **Try GPT-5.6 Sol** for advanced coding, research, and long-running agentic work. - **Audio or video? Use a Claude or GPT model.** The open-weight models (GLM, DeepSeek, Kimi) read text and images, including images from your files, but not audio or video. PDFs work on every model: Avi reads their text and looks at their pages as pictures. When a model opens a file it can't read, it tells you so. ## Where to set the model - **Org-level default** — in org settings. Every project in the org picks this up unless overridden. - **Per-project** — override in project settings. Lets you use Opus for your most demanding project while keeping Sonnet everywhere else. - **Per automation** — every automation runs on **GLM 5.3 Flash** unless you pick another model on its Setup tab. It never follows your chat model. See [Automations](/docs/background-agents). Voice turns use the project's configured chat model too — speaking to the agent and typing to it go to the same model. ## Claude Fable 5.1 and data retention Every model in the catalog runs without its provider keeping your prompts or training on them — except Claude Fable 5.1, which is only served with data retention on: the model provider keeps prompts and outputs from Fable 5.1 requests for 30 days for safety monitoring. Only Fable 5.1 is affected; no other model's data handling changes. Until an org owner or admin turns on **Claude Fable 5.1 data retention** in org settings, nobody in the org can select it. Turning it off makes Fable 5.1 unavailable at once. ## Background work Some of Avi's work happens without you asking: sorting events into your feed, summarizing long conversations, telling whether a second thing you said finishes the first, and app chores like labeling a new task. All of it runs on **GLM 5.3 Flash**, whatever chat model you pick, and it's billed at GLM 5.3 Flash's rates. The updates in your feed are then written by **Claude Sonnet 5.5**. If your org's **Allowed models** list leaves out GLM 5.3 Flash, that background work pauses, feed updates included, until an owner or admin allows it again. Leaving out Claude Sonnet 5.5 pauses the feed's updates the same way. Your chats keep using the models you allowed. An automation on the default needs an allowed model picked on its Setup tab. --- # Billing Avi has one plan. You start on **trial credits**, and when they run out you **subscribe** — $99 per person per month, or $79 per person per month billed annually. Everything Avi does draws from a single pool of **credits**: chatting, tools that call a model, automation runs, web searches, and app actions. Your **account** carries the credit balance, and every feature is available whether you're on trial credits or subscribed. ## Credits - **AI requests** consume credits based on the model and how much work the request does — a typical chat turn is 5–30 credits, more for multi-step tool use. Each model has its own rate in credits per million tokens, so a fast model costs a fraction of a frontier one. - **Web search and app actions** (posting to X, hosting a site, and so on) run on the same credit system and are included at no charge today. - One credit is one cent of usage: a $25 top-up is 2,500 credits. - Automations each carry a **daily credit ceiling** — 100,000 credits a day by default, on every model — so nothing can quietly run away — see [Automations](/docs/background-agents#max-daily-spend). ## Your trial - **1,500 credits** when you create an account. No card required. - **1,500 more when you connect your first account** — see below. - Trial credits expire 30 days after they're granted. Everything works during the trial: every model, every app, teammates, automations. - When your trial credits are gone, subscribe to keep going. ### The first-connection offer Avi is at its best when it can see your email and calendar — that's when morning briefings, meeting prep, and "what needs a reply?" start working. So the first time your account connects Google (or Microsoft), Avi adds **1,500 credits** to your trial. Once per account. Connecting Google also sets up your Email, Calendar, and Drive apps automatically — see [Getting Started](/docs/getting-started). You stay in control: the connect screen lets you choose exactly which permissions to grant, and you can disconnect anytime from the settings of the app that uses the account. ## Subscribing — $99 per person per month - **$99 per person per month**, or **$79 per person per month billed annually** ($948 per person per year). Choose either at checkout. - **10,000 included credits per person every month**, pooled across everyone on your account and every org under it. Five people = 50,000 credits a month that anyone can spend. On the annual plan the credits still arrive monthly — paying yearly changes the price, not the pace. - Everyone you invite is a person on your subscription. Adding someone mid-cycle takes effect immediately; you're billed for the seat in full at your next renewal — no prorating, no partial-month math. - **Top-ups**: when you need more than the monthly grant, add 2,500 credits for $25 any time from the Billing page. Top-up credits never expire. - **Auto-refill** (optional): when your available credits drop to 200 or less, Avi charges $25 and adds 2,500 credits automatically. It's capped by a monthly budget you control (default $100) so surprises are bounded. - Cancel or update your card any time in the Stripe Customer Portal. ## How the numbers work The Billing page shows your credit picture in a few numbers: - **Monthly Credits Used** (subscribed) — how many credits you've spent this month against the included grant (`people × 10,000`). If you add someone mid-month, the grant grows right away. - **Trial Credits** (on trial) — what's left of your trial, and when it expires. - **Extra Credits Balance** — your persistent pool: top-ups, auto-refills, and any promo grants. Never expires, carries across cycles. Credits are spent in that order: the monthly grant first, then trial credits, then extra credits. At cycle end, the monthly grant resets; unused monthly credits don't roll over (they're part of your subscription, not a balance you own). Extra credits always carry over. ## Where to manage it - **User Settings → Billing** — see your trial or subscription, credit usage, daily breakdown, subscribe, toggle auto-refill, top up. - **Manage Billing** button — opens the Stripe Customer Portal. From there you can update your card, view past invoices, or cancel. - **Org Settings → Analytics** — per-org credit usage if you want to understand spend by project. ## Inviting teammates Anyone can invite teammates, on trial or subscribed. Each person you add shares the account's credits. If you're subscribed, the new seat activates on accept and appears on your next invoice at your per-person rate; removing a member drops your seat count at the next renewal (no mid-cycle refund). ## Who can change billing Billing is **account-scoped** — the account owner is the only one who can subscribe, cancel, top up, or update the card. Exactly one owner per account. --- # Appearance Avi has four themes: **Light**, **Dark**, **System**, and **Natural**. Switch between them in the account menu. ## Light, Dark, and System **System** is the default: it matches your device's light or dark setting. **Light** and **Dark** keep Avi in one look all day. None of them color the interface. In the Avi mobile app, the theme follows your phone's light or dark setting. ## Natural Natural follows the time of day where you are: - **Light by day, dark by night.** The app is light from 6 am to 7 pm and dark overnight. The switch happens at the same times every day, so you always know what to expect. - **A living sky.** The whole window is lit by a sky that changes through the day — soft peach and periwinkle at dawn, clear blue at midday, gold and coral through sunset, deep blue after dark. The light moves continuously, so a sunrise or sunset plays out while you work. - **A landscape behind it all.** A slow, quiet landscape plays behind the window, a little stronger or softer to suit the light. It is hidden if your device is set to reduce motion, and it never plays on phones. Natural reads only your device's clock. It never asks for your location. --- # Apps If none of the [existing apps](/docs/using-apps) cover what you need, write your own. Avi's CLI turns a small TypeScript file into a deployable app that your agent can use in any project. ## The idea An app is one installable unit. It can provide **tools** the agent or a panel calls, a **background handler** woken by **triggers** (a schedule, project events, or a webhook), its own **data** stores, project **settings**, and optional **React panels**. An app also decides how its own activity is summarized in the project's Updates feed — which events matter, which are just counted. You author it locally, bundle it with the CLI, and deploy. The CLI hosts the code for you; you don't need to run any infrastructure. There is one app shape: `defineApp({ ... })`. Tools are on-demand functions. The optional `handler(event, context)` is the app's autonomous worker — Avi wakes it whenever a declared trigger fires. Everything (tools, handler, panels) shares the same app `data`, `settings`, and `capabilities`. Great candidates: - An internal API your team uses that Avi doesn't have an app for. - A proprietary workflow only your company does. - A bespoke integration — Slack automations, internal dashboards, anything. ## Install the CLI ```bash npm install -g @avi-hq/cli # or build it from source: npm -w @avi-hq/cli run build ``` ## Sign in ```bash avi login ``` Prints a link and a code. Open the link in a browser and enter the code to connect the CLI to your Avi account, then pick which org your apps belong to. `avi switch-org` changes that org later, and `avi logout` signs the CLI out. ## Scaffold an app ```bash avi init my-app ``` Writes a starter project into the current directory, with `my-app` as the app's name (without a name, the directory's name is used). Pass `--dir ` to create it in a new folder instead. Files that already exist are kept unless you add `--force`. The starter includes: - `app.ts` — the entry point where you declare app metadata plus tools, an optional background `handler` with its `triggers`, `data` stores, `settings`, and panels. - `@avihq/apps-sdk` as the authoring SDK for types like `AppToolContext`. - This SDK is type-only; Avi supplies the runtime when your app runs. - An optional `src/` area you can add later for supporting code. - A skill file for authoring agents (e.g. Claude Code) so they can help you write tools and the background handler. - A `package.json` wired up with the right scripts. ## App icon Every app **must** declare a top-level `icon` — it can't deploy without one. The icon is **an image** (no built-in glyph names): an `https://` image URL, a `data:image/...` URI, or — most common — a relative path to an image you bundle next to `app.ts`. The CLI inlines a bundled path at deploy, so you just commit the file: ```ts export default defineApp({ name: "customer-success", title: "Customer Success", // display name — free casing, shown wherever the UI names the app summary: "Flags customer accounts at renewal risk.", // the app's one-liner on every card and list icon: "./icon.svg", // bundled next to app.ts — .svg/.png/.jpg/.gif/.webp, up to 96 KB // ... }); ``` Your icon is what users see for the app across Avi — in the Apps list, the app's rail tile, and beside the updates it posts to the project feed. **Icons render monochrome, everywhere.** You supply **one** icon file, and Avi paints it as a flat silhouette in the current theme color — light and dark, active and dimmed, every surface as one set. The color in your file is never shown; only its **shape** matters. The silhouette is derived from your icon's **alpha channel**, so a mark on a **transparent background** works with no extra effort. That's the only real requirement: - Use an SVG with a **transparent background**. A `0 0 64 64` (or `0 0 24 24`) viewBox works well. Drawing in `currentColor` or any single color is ideal — the shape is what ships. - For a real brand mark, the [Simple Icons](https://simpleicons.org) set is a great source — their marks are single-color with transparent backgrounds, exactly the right shape. - No logo? A clean one- or two-letter monogram or a minimal glyph is perfect. ### When your logo needs an `iconMono` If your mark sits on an **opaque background tile** — a colored square with a knocked-out white glyph — its alpha channel is the whole square, so the silhouette would come out as a solid block. Only in that case, add a second file: ```ts export default defineApp({ icon: "./icon.svg", // your mark; its alpha channel is the shape iconMono: "./icon-mono.svg", // transparent, single-color version of the mark }); ``` When `iconMono` is present its alpha is used for the silhouette instead of `icon`'s. Most apps never need it. (A panel's own `ui.panels[].icon` may also use a built-in glyph name — panels are Avi's chrome, not your brand. The app's `icon` is the identity shown on the rail and the panel title bar.) ## Write your tools Edit `app.ts`. Import authoring types such as `AppToolContext` from `@avihq/apps-sdk`, then declare the app's **name**, **title**, **summary**, **icon** (required — see below), and the list of **tools**. The `name` is the app's identity — kebab-case only (it becomes the tool prefix and the app's public subdomain), so it can never contain capital letters. The optional `title` is the human-facing display name with free casing (`"HubSpot"`, `"CRM"`, up to 60 characters) shown wherever the UI names the app; when omitted, the UI shows the `name`. The background `handler`, `data`, `settings`, and React panels live beside tools in the same `defineApp(...)` declaration. The SDK describes the handler contract; the deployed runtime layer provides the actual `context` implementation. Each tool has: - **Name** — what the agent calls it. - **Description** — what it does (the agent reads this to decide whether to use it). Write it for the model: it can be long, technical, and full of parameter guidance. - **Summary** (optional but recommended) — one short plain sentence shown to people in the Apps and Tools views instead of the description. Keep it under ~80 characters; when omitted, the UI shows the description. The app itself takes only a required `summary` — its one-liner on every card and list. There is no app-level `description`: long-form prose about what the app does and how it works belongs in `docs/README.md` (see [Documentation](#documentation)). - **Input schema** — JSON Schema or Zod schema of the arguments. - **Output schema** — JSON Schema (or Zod) of exactly what the handler returns. Agents call tools from code-mode programs, where each tool is a TypeScript function signature; the output schema becomes its return type (`=> Promise<{ deal: { id: string; … } }>`), so a program reads the right keys without a throwaway call. Without one, the result types as `unknown`. Declare it on every tool: `type: "object"` at the top (a union goes inside a property), the real key names, `required` on keys that are always present, `enum` for fixed values, `type: [T, "null"]` for anything that can be null. Describe the shape only; meaning belongs in the description, which shouldn't restate the shape. It is **descriptive** — Avi never validates or rejects an output against it — so change it in the same edit as the handler's return, and check real results in your tests with `outputSchemaMismatches(schema, result)` from `@avihq/apps-sdk/testing`. `avi build` warns about tools without one and refuses a schema that isn't a single object type or is larger than 16 KB. - **Callable from** (optional) — `agent` (chat assistant), `ui` (React panels), and/or `handler` (the app's own background handler), as a single value or an array. **Defaults to all three.** This controls who may call the tool. The handler's `context.apps.invoke` reaches any of the app's tools callable from `agent` or `handler`, so mark an internal helper `callableFrom: "handler"` to keep it out of the agent's tool list and the panels. - **Handler** — the function that runs. - **Secret inputs** (optional) — per-project secret references the backend resolves before your handler runs. Each field is required by default; declare `optional: true` on a field to let callers omit it (your handler then sees `undefined`) — useful when only some of a tool's actions need the credential. Declare `list: true` to accept an **array** of secret keys (your handler receives the array of resolved values in the same order, up to 20 per call) — useful when one call needs several credentials at once. A list field can also declare `allowedFromSetting: ""`, naming one of your app's `secret`-type settings: the backend then rejects any key a project admin hasn't selected in that setting, **fail-closed** — with nothing selected, no key resolves. Pair it with `resolve: false` on the setting (see below) when the setting is purely an allowlist. ```ts tools: { "get-deal": { description: "Get one deal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false }, outputSchema: { type: "object", properties: { deal: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, stage: { type: "string" }, closed_at: { type: ["string", "null"] } }, required: ["id", "title", "stage", "closed_at"], }, }, required: ["deal"], }, async handler({ id }, context) { return { deal: await loadDeal(context, id) }; }, }, } ``` A `search` tool uses the shared envelope: `outputSchema: searchOutputSchema({ kinds: ["deal"], hitProperties: { stage: { type: "string" } } })`. Use `callableFrom` to narrow normal project-level callers, such as a panel-only helper: ```ts tools: { "refresh-cache": { description: "Refresh cached customer data from the panel.", inputSchema: { type: "object", additionalProperties: false }, outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] }, callableFrom: "ui", async handler() { return { ok: true }; }, }, } ``` ## The background handler Beyond on-demand tools, an app can run autonomously. Declare what wakes it under `triggers`, then write **one** `handler(event, context)` — Avi calls that single function for every wake-up. An app declares no agents of its own (a manifest with an `agents` field is refused at deploy) and has nothing extra to approve; the handler runs under the app's one declared capability set, the same `data`, and the same `settings` as its tools. ```ts import { defineApp, type AppEvent } from "@avihq/apps-sdk"; export default defineApp({ name: "customer-success", summary: "Flags customer accounts at renewal risk.", icon: "./icon.svg", capabilities: ["data:read", "data:write", "updates:write", "logs:write"] as const, data: { state: { type: "kv", scope: "project" }, }, settings: { lookbackDays: { type: "number", label: "Look-back window (days)", default: 14 }, }, // What wakes the handler. All three are independent and optional. triggers: { schedule: "0 * * * *", // a 5-field cron in UTC, or an array of them events: ["billing.invoice.*"], // event types from the project event log webhook: false, // true to expose a public inbound URL }, async handler(event: AppEvent, context) { if (event.type === "schedule.ticked") { const since = Date.now() - context.settings.lookbackDays * 86_400_000; const last = (await context.data.state.get("last-sweep")) ?? null; // ...do the work: read data, call tools, publish updates and events... await context.updates.publish({ subject: "Renewal risk: Acme", body: "Usage down 40% since the last invoice", }); // Durable history (schedule/webhook wake-ups are NOT auto-logged): await context.events.publish({ type: "customer-success.sweep.completed", data: { since, previous: last }, }); await context.data.state.set("last-sweep", new Date().toISOString()); } }, tools: { // ...on-demand tools the chat agent and panels call... }, }); ``` ### Triggers `triggers` declares what wakes the handler. All three fields are independent and optional — set any combination: | Trigger | Shape | The handler is called with | |---|---|---| | `schedule` | a standard 5-field cron string, read in UTC, or an array of them (e.g. `"*/10 * * * *"`, `["0 9 * * 1-5"]`) | a `schedule.ticked` event on each tick | | `events` | an array of dotted event-type filters, `*` wildcard allowed (e.g. `["gmail.message.*"]`) | the matching `AppEvent` from the project event log | | `webhook` | derived automatically when you declare top-level `webhooks` (see [Webhooks](#webhooks)) | a `.webhook.` event per inbound request | Schedule wake-ups are **ephemeral handler inputs** — not written to the event log; call `context.events.publish(...)` to record durable history. A webhook wake-up writes a lean audit event automatically. An `events`-triggered handler reacts to events that already live in the log. ### The `event` argument Every wake-up passes an `AppEvent`: ```ts { id, orgId, projectId, appId, // the app that published it, or null for system sources type, // dotted, e.g. "schedule.ticked", "gmail.message.received" tags, // free-form labels for filtering contactIds, taskIds, noteIds, // first-class entity associations this event is about occurredAt, // when the underlying thing actually happened data, // arbitrary structured payload createdAt, } ``` Branch on `event.type` to handle each trigger. For a scheduled app, that's just `schedule.ticked`; for an event-driven one, it's whatever types you subscribed to. ### Runtime behavior The handler runs as a **system actor**: `context.user`, `context.userId`, and `context.timezone` are `null` (nobody is reading a handler run as it happens — in tool calls, `context.timezone` is the IANA zone of the person the call is for, so tools can show times on their clock). The handler ships in your app's own bundle, beside its tools. - **Schedule** — every project that enabled the app gets its own ticks for each declared cron, so a slow project never holds up another's. Change the crons and redeploy, and every project picks up the new schedule. A schedule that isn't a 5-field cron runs every five minutes instead. - **Events** — when a matching event is appended to the project event log, Avi dispatches it to the handler. - **Webhook** — an inbound request to the app's per-install public URL wakes the handler with a `.webhook.` event (see [Webhooks](#webhooks)). A handler invocation may run for up to 15 minutes. Keep per-tick work bounded and resume across ticks using a `"kv"` data store (persist a cursor / last-run marker, as the example does with `last-sweep`). ### Publishing events A handler (or a tool) with `events:write` can append to the project event log: ```ts await context.events.publish({ type: "customer-success.sweep.completed", occurredAt: new Date(), // when it actually happened; omit for the server clock tags: ["renewal"], data: { riskScore: 0.82 }, dedupeKey: "sweep:2026-06-16", // optional — collapses duplicate publishes of the same source item }); ``` Other apps can subscribe to these types via their own `triggers.events`, and Avi's curation engine reasons over the log to surface project updates. Read the log with `events:read` (`context.events.list(...)` / `context.events.get(id)`). Two optional envelope fields tune how the curation engine treats an event: - `embedText` — a short, pre-trimmed projection (headline + leading content) that Avi embeds and matches on *instead of* the whole `data` payload, so long bodies and id/link metadata never dilute placement. - `curatable: false` — keeps the event off the curation engine entirely (right for high-churn activity your other events already summarize). This only narrows: `true` is ignored — an app can't force itself into the feed. For events you *want* the engine to be great at, shape `data` with the `curatableEventFields` helper from the SDK. It assembles the standard field names the engine understands — `subject`, a full `body` with honest truncation flags, `thread_id` / `record_id` (stable identities that group later events about the same thread or record onto the same update), `conversation_id`, `participants`, `mentions_user` / `from_user`, `provider` / `direction`, `connection`, and a deep link — and returns `{ data, embedText }` ready to publish: ```ts import { curatableEventFields } from "@avihq/apps-sdk"; const { data, embedText } = curatableEventFields({ subject: ticket.title, body: ticket.description, // full text — capped + flagged for you recordId: ticket.id, // later events about this ticket group onto its update provider: "acme-desk", direction: "inbound", aviLink: panelUrl, extra: { ticket_id: ticket.id, severity: ticket.severity }, }); await context.events.publish({ type: "support.ticket.opened", data, embedText }); ``` Every field is optional — a plain `data` payload still flows through the engine unchanged. #### Telling the feed what your events mean Avi's feed follows each real piece of work — a deal, a ticket, an incident — as one topic, and labels it with tags: `engineering`, `product`, `sales`, `finance`, `support`, `schedule`. You never write the feed. Add a `curation` section to `defineApp` to say what your events mean, and Avi places them. ```ts curation: { // big = a real change of state (won, shipped), never a volume; routine is // the default; bookkeeping = joins its topic silently; ignore = never shown. events: { "support.ticket.escalated": "big", "support.ticket.viewed": "ignore" }, // The tags a type carries (the field keeps its old name, `topics`). topics: { "support.ticket.*": ["support"] }, // Messages people wrote. Everything else is activity on a record. conversation: ["support.message.*"], // data keys that tie later events to the same topic, beside record_id and thread_id. matters: { attachBy: ["ticket_id"] }, }, ``` - Keys are an exact type or a `family.*`; the most specific entry wins. Name only types you publish. - Without a `curation` section, `*.message.*` types are read as messages. Once you declare one, `conversation` decides. - Avi tags messages itself, and any record type you don't map. A type you don't grade is routine, except plumbing like `*.sync.*` and `*.webhook.*`, which defaults to `ignore`. - If your app keeps someone's to-do list, declare `matters.todos`. `list` names one of your tools that lists the open tasks (`{ tool, input? }`; each item needs an `id` and a `name` or `title`, and may carry `notes_preview` and `url`). `events` names the types that carry a task's current state (`record_id` and `name` in `data`): publish one when a task is renamed, completed, cancelled, deleted, or reopened. Work that belongs to a task then lands on it. - `guidance` and `upcoming` (and `matters.seed`, `matters.guidance`) were for topic summaries Avi no longer writes. Deploys still accept them, but nothing reads them, so remove them. - Publish an Update yourself (`updates:write`) only for something no single event says, like a digest. ### Calling your own tools from the handler A handler can call **its own app's** tools (any callable from `agent` or `handler`) through `context.apps.invoke` — no capability needed: ```ts const result = await context.apps.invoke("support_tickets-poll", { since: cursor }); ``` Rules: - The tool must belong to the same app (runtime name `_`). Apps cannot invoke other apps' tools — that was removed. If your app needs data another app owns, publish and subscribe to events; sending email or messages as the project is a first-party feature, so a third-party app that needs to notify someone publishes an update/event for the project's own surfaces to act on, or sends through its own provider with its own credentials. - For tools with `secretInputs`, pass the secret key exactly as a normal tool caller would — the backend resolves the real secret value before invoking the tool. ## Webhooks A webhook lets an external service push events to your app in real time, instead of you polling for them. Declare them with the top-level `webhooks` array. Avi mints one **public URL per install** (per project that enables your app) on your app's own subdomain of the public user-content domain — `https://.avi.tools/api/v1/webhooks/apps/` (the same `avi.tools` subdomain that serves the app's pages, never the product/API domain); the project admin pastes it into the vendor's webhook settings. ```ts export default defineApp({ name: "my-app", capabilities: ["events:write"], webhooks: [ { name: "events", // kebab-case; the URL segment + event suffix description: "Vendor event deliveries.", respond: "ack", // "ack" (default) or "sync" — see below secrets: [ { name: "signing_secret", description: "Vendor signing secret, for verifying each request." }, ], }, ], handler, }); ``` An inbound request wakes your handler with a `.webhook.` event — route on `event.data.hook`: ```ts import { readWebhookRequest, webhookBodyText } from "@avihq/apps-sdk"; import crypto from "node:crypto"; export async function handler(event, context) { if (event.data?.hook !== "events") return; const request = readWebhookRequest(event); if (!request) return; // Verify the request is really from the vendor — you have the verbatim bytes. const body = webhookBodyText(request); const ok = verifyHmac(request.secrets.signing_secret, request.headers, body); if (!ok) return { status: "skipped" }; const payload = JSON.parse(body); await context.events.publish({ type: "vendor.thing.happened", data: payload }); } ``` How it works: - **The URL is the credential.** It carries a secret token; treat it like a password. Rotating it (in the app's settings) revokes the old URL immediately; disabling it is a kill switch. - **You verify authenticity.** Avi passes the **verbatim request bytes + headers** through untouched. Declare the vendor's signing secret under `secrets`; the project admin binds it to a project secret key in settings, and it arrives on `request.secrets.` (never written to the event log). Use it to check the vendor's signature (HMAC) yourself. - **Verification handshakes are automatic.** Avi answers generic subscription challenges (Slack `url_verification`, Meta `hub.challenge`) for you without waking your handler — so most webhooks stay `respond: "ack"`. - **`ack` vs `sync`.** `ack` (default) returns `202` immediately and runs your handler in the background — right for normal event delivery. `sync` runs your handler inline (bounded by `timeoutSeconds`) and relays its returned `{ status?, headers?, body? }` to the sender — use it only when the vendor needs a specific response body. - **Read the body** with `readWebhookRequest(event)` then `webhookBodyBytes` / `webhookBodyText` / `webhookBodyJson`. Configuring the URL and binding secrets is done per project in the app's settings — see [Apps → Webhooks](/docs/using-apps#webhooks). **Reading your own webhook URL from code.** Sometimes the app itself needs its ingress URL — for example to embed it as a contact form's `action` in a site it publishes. Declare the `webhooks:read` capability and call `context.webhooks.url(name)` with a declared webhook name: it returns this install's public URL, creating the endpoint on first use (the same URL the project admin sees in settings). An undeclared name rejects. ## React panels Apps can also expose React panels, which open from the rail (the dock on a phone) in every project that enables the app. Panels run in a sandboxed iframe, so your React code never executes inside Avi's main app tree and cannot access Avi auth tokens, local storage, cookies, or the parent DOM. ```ts export default defineApp({ name: "customers", summary: "Customer dashboard.", icon: "./icon.svg", capabilities: ["data:read", "data:write"] as const, data: { // A KV store for the app's own state. state: { type: "kv" }, // A queryable record collection. customers: { type: "collection", fields: { name: { type: "string", required: true }, status: { type: "enum", enum: ["lead", "active", "at-risk", "churned"] }, notes: { type: "string" }, }, indexes: [{ fields: ["status"] }], fullText: ["name", "status", "notes"], vector: ["name", "notes"], globalSearch: { kind: "customer", titleField: "name", snippetField: "notes" }, }, }, ui: { panels: { dashboard: { title: "Customers", icon: "users", entry: "./ui/Dashboard.tsx", }, }, }, tools: { // ... }, }); ``` A collection's `globalSearch` opts its records into Avi's **global search** — the chat agent's one search tool and the chat composer's `+`-mention picker — so your records show up next to native Tasks and can be referenced directly in a message (the composer inlines the record for the agent, along with a pointer to your app's `read` tool when you expose one). Set `titleField` (which field is the result title) and, optionally, `snippetField` (the subtitle), `kind` (a human label like `"customer"`), and `searchFields` (which fields to text-match; defaults to `fullText`, else the title field). Every field you name must exist on the collection, or the deploy is rejected. Omit `globalSearch` entirely and the collection stays private to your app. Panel code is normal React. Import components from Avi's component library, `@avihq/ui/panel`, and bridge hooks such as `useAvi` from `@avihq/apps-sdk/react`: Panel icons can use Avi's built-in names, relative SVG/PNG/JPEG/GIF/WebP paths that the CLI bundles at deploy time, compact `data:image/...` URIs, or HTTPS image URLs. ```tsx import { AppShell, Button } from "@avihq/ui/panel"; import { useAvi } from "@avihq/apps-sdk/react"; export default function Dashboard() { const avi = useAvi(); return ( ); } ``` Limits: - An app can define up to five panels. Prefer one, with extra surfaces as tabs inside it. - Each panel bundle must be 4 MB or smaller. - Panel entries must be relative `.tsx`, `.ts`, `.jsx`, or `.js` files. - Browser panel bundles cannot import Node built-ins such as `fs`, `path`, or `node:*`. - Panel data access goes through the Avi bridge and is checked against the same approved app capabilities as tools. `@avihq/ui/panel` carries the layout, form, dialog, and record components (`AppShell`, `Select`, `ConfirmDialog`, `RecordPage`, and more), all themed and voice-operable; lists come from `@avihq/ui/data-table` (`RecordBrowser`, `DataTable`). `@avihq/apps-sdk/react` carries the bridge hooks: `useAvi`, `useTheme`, `useAviView`, `useLiveData`, `useAppData`, `useAppTool`, and more. (The SDK's older `Button`/`Input`/`PanelHeader` primitives still render in already-deployed panels, but new panels should use `@avihq/ui/panel`. `useAppRecords` is a deprecated alias of `useAppData` — record collections are now declared data stores reached through `avi.data.`.) For the full authoring guide, including the manifest contract, bridge APIs, theming, limits, security model, and troubleshooting, see [App React UI](/docs/app-react-ui). ## Public pages and sites Apps can publish two kinds of public surfaces, and they solve different problems: **Pages** (`ui.pages`) are React bundles served inside Avi's hardened shell on your app's own subdomain (`.avi.tools/`), rendering data from your app — booking pages, shared views, per-thing links. Pages are unlisted by default; set `indexable: true` on a page's manifest entry when it's public content that should appear in search engines. **Sites** (`context.sites`) are real static websites on a dedicated sites domain, each on its **own subdomain** — full HTML documents and assets you author, served verbatim and fully SEO-capable (your own ``, meta tags, everything). Requires the `sites:read` / `sites:write` capabilities. ```ts // First touch mints a random host like https://falcon-harbor-8214.avi.site/ const url = await context.sites.url("main"); // Let the user pick a custom subdomain — check availability first const { available, reason } = await context.sites.checkHost("acme"); if (available) await context.sites.claimHost("main", "acme"); // → https://acme.avi.site/ // Publish files. Small text files go inline via `body`; larger files // (images, video, anything) declare `sizeBytes` and come back with an // upload URL. const result = await context.sites.publish("main", [ { path: "/", body: "<!doctype html><html>…</html>" }, { path: "/styles.css", body: "body { … }" }, { path: "/hero.jpg", sizeBytes: heroBytes.byteLength }, ], { replace: true }); // PUT each large file's bytes to its upload URL (send the file's content // type as the Content-Type header), then confirm to take them live. for (const upload of result.uploads ?? []) { await fetch(upload.url, { method: "PUT", headers: { "content-type": "image/jpeg" }, body: heroBytes }); } await context.sites.confirm("main", ["/hero.jpg"]); // Take files (or the whole site) down await context.sites.remove("main", ["/old-page.html"]); ``` Hosts are 3–40 lowercase letters, digits, or hyphens, globally unique, first-come-first-served. Content types are inferred from the file extension when you don't declare one (extensionless paths serve as HTML); almost anything is allowed — only executable file types are refused. Inline bodies are UTF-8 text up to 256 KB; anything bigger goes through the upload flow. Limits: 25 MB per file, 10,000 files and 1 GB per site. Upload URLs expire after 15 minutes; `confirm` is safe to call again if some uploads finish late. ## Data stores and settings An app declares two separate things under `defineApp`: - **`data`** — the app's own persistence: a **map of named stores** the tools, handler, and panels read and write. - **`settings`** — a **project-scoped field schema** a project admin fills in once per project; the app reads the resolved values (read-only). ### `data` — named stores `data` is a map keyed by store name. Each store is either a **KV store** (`type: "kv"`) or a **record collection** (`type: "collection"`), and each declares its own `scope`: ```ts export default defineApp({ name: "customers", summary: "Customer records.", icon: "./icon.svg", capabilities: ["data:read", "data:write"] as const, data: { // KV store, isolated per enabling project (the default). state: { type: "kv", scope: "project" }, // Record collection, shared across the org's enabled projects. customers: { type: "collection", scope: "org", fields: { name: { type: "string", required: true } }, fullText: ["name"], globalSearch: { kind: "customer", titleField: "name" }, }, }, tools: { // ... }, }); ``` Each declared store becomes a typed handle on **`context.data.<storeName>`** (and, in panels, `avi.data.<storeName>`): | Store `type` | `context.data.<store>` methods | |---|---| | `"kv"` | `get(key)`, `set(key, value)`, `has(key)`, `delete(key)`, `listKeys(options)` | | `"collection"` | `create(record, { id? })`, `get(id)`, `update(id, patch)`, `delete(id)`, `list(options)`, `search(options)` | ```ts // KV store const config = (await context.data.state.get("config")) ?? defaults; await context.data.state.set("config", { ...config, lookbackDays: 30 }); // Record collection await context.data.customers.create({ name: "Northstar Labs" }); const hits = await context.data.customers.search({ text: "northstar" }); ``` For complete text-search pagination, supply an explicit record-field order and keep the text, filters, and order unchanged between pages: ```ts const page = await context.data.customers.search({ text: "northstar", orderBy: { field: "name", direction: "asc" }, limit: 25, // cursor: previousPage.nextCursor, }); // page.records; page.nextCursor is null when there are no more matches. ``` Text-only search with `orderBy` matches full-text prefixes or substrings and uses the same stable field/id cursor ordering as `list`. It searches the complete matching set without the relevance engine's candidate cap. It returns `searchSignals: ["text"]` but no relevance score. Without explicit ordering, or when a vector query is supplied, search remains relevance-ranked and bounded, with no continuation cursor. Blank text falls back to `list`; punctuation-only text is not a listing. Record `update(id, patch)` merges a partial patch under a database row lock. It does not support conditional updates: a separate read followed by an update is not atomic, so concurrent read-modify-write operations can lose increments or overwrite changes to the same field. The row lock protects the patch operation itself, not a transaction across calls, collections, or an external provider. Scope is **per store**, not app-wide: - **`scope: "project"` (default)** — isolated per enabling project; each project that enables the app gets its own copy. - **`scope: "org"`** — one shared partition across every enabled project under the org; all of them read and write the same data. - **Fixed at deploy.** A store's scope is set when it is first deployed; changing it would orphan the old partition. - **Independent of install scope.** A project-installed app can still declare `scope: "org"` stores. - **Global search merges both.** Searching from a project covers its project-scoped app records *and* the org-scoped records of any enabled app. There is no app-wide `dataScope` and no top-level `context.get`/`context.set`/`context.records.collection(...)` — always reach data through a declared store: `context.data.<storeName>`. Files (`context.files`) and the project primitives (tasks, contacts, notes, updates) always stay bound to the invoking project. ### `settings` — project-scoped admin fields `settings` declares fields a project admin fills in for the app in that project. The app never writes them — it reads the resolved values from **`context.settings.<field>`** (typed, read-only): ```ts export default defineApp({ name: "crm-sync", summary: "Sync CRM data for a project.", capabilities: ["data:read", "data:write"] as const, settings: { baseUrl: { type: "string", label: "API base URL", required: true }, region: { type: "enum", label: "Region", options: ["us", "eu"], default: "us" }, pageSize: { type: "number", label: "Page size", default: 50 }, verbose: { type: "boolean", label: "Verbose logging", default: false }, }, tools: { // ... }, }); ``` ```ts const res = await fetch(`${context.settings.baseUrl}/accounts?region=${context.settings.region}`); ``` Field types are `string`, `number`, `boolean`, `enum` (with `options` — value strings, or `{ value, label }` objects when the stored value and the display text differ, e.g. value `avimail` shown as “Avi Mail”), `timezone` (a searchable timezone picker — value is the IANA zone name), `instructions` (a rich text editor — value is a `string`), `list` (a tag input — value is a `string[]`), `integration` (a picker over the project's connected Integrations), and `secret` (a picker over the project's Secrets — the setting stores only the secret's key, and the resolved value is delivered to your code per invocation on `context.settingSecrets.<field>`; supports `multiple`). Use `secret` for app-level credentials an admin binds once — your app never reads the secret store itself and needs no extra capability; the resolved value is handed to each invocation and never persisted. A `secret` field may also declare `resolve: false`, making it a **key-only reference**: the platform stores the selected key(s) but never delivers the values to your app (the field is absent from `context.settingSecrets`). Use this for allowlist-style settings — typically the target of a secret input's `allowedFromSetting` — where the admin's selection *authorizes* keys rather than binding a credential; key-only pickers also offer integration-derived secrets (e.g. AWS short-term credentials), not just user-created ones. A `list` field can also declare `optionsSource: { type: "integration-resource", service, resource, setting }` to offer live options fetched from a connected integration (for example `google` / `calendars` scoped to a sibling `integration` field) — admins pick from a dropdown instead of typing values, and can still add manual entries. `required` and `default` control whether a field is guaranteed present at runtime — required or defaulted fields are always set; an optional unset field reads as `undefined`. Use `settings` for per-project configuration an admin should own; use a `"kv"` store for state the app itself manages. ### Vendor credentials — bring your own An app that talks to an outside service (Google, Slack, HubSpot…) connects to it with **its own** OAuth client or API key, stored as a `secret` setting. The accounts a user connects to Avi's own apps (Email, Calendar, Drive, CRM, Messages, Code…) are never shared with other apps — those credentials only ever reach code Avi authors, which is what keeps every connected account inside the vendors' terms. ## Capabilities and scopes Apps declare the capabilities they need in `defineApp({ capabilities })`. Avi asks an admin to approve those capabilities when the app is enabled for a project. Apps install per-project (there is no org-wide install). If an app is redeployed with new capabilities later, each project that uses it re-approves the new capabilities from Project Settings before the app runs again. Apps made by Avi skip this: they are granted the capabilities they declare, with no approval step. ```ts export default defineApp({ name: "crm-sync", summary: "Sync CRM data for a project.", capabilities: [ "data:read", "data:write", ] as const, tools: { // ... }, }); ``` The SDK uses that list for type-safety. For example, `context.files.put(...)` is only available when the app declares `files:write`, and `context.data.<collection>.create(...)` (a collection store) is only available when it declares `data:write` (declared data stores are covered by the `data:*` scopes). | Scope | Context helper | What it allows | Boundary | |---|---|---|---| | `data:read` | `context.data.<kvStore>.get/has/listKeys`, `context.data.<collection>.get/list/search` | Read this app's declared data stores — KV stores AND record collections (incl. full-text + vector search). One scope covers both store types. | Per-store data partition (project, or org when the store declares `scope: "org"`) + app | | `data:write` | `context.data.<kvStore>.set/delete`, `context.data.<collection>.create/update/delete` | Write this app's declared data stores — KV stores AND record collections (and their search indexes). One scope covers both store types. | Per-store data partition (project, or org when the store declares `scope: "org"`) + app | | `updates:read` | `context.updates.list/get/search` | Read project feed updates (mutable, one living update per concern); search to find an existing one. | Invoking project | | `updates:write` | `context.updates.publish` | Publish an Update to the project feed — appends to an existing one when a `dedupeKey` collides. A new Update push-notifies project members by default; pass `notify: false` for digests/rollups that shouldn't buzz anyone. | Invoking project | | `events:read` | `context.events.list/get` | Read the project event log (filter by type, tag, or associated entity). | Invoking project | | `events:write` | `context.events.publish` | Append an event to the project event log. | Invoking project | | `tasks:read` | `context.tasks.get/list` | Read the read-only archive of Avi's former built-in tasks. Current tasks live in the Tasks app, and an app can't call another app's tools. | Invoking project | | `contacts:read` | `context.contacts.get/list` | Read the read-only archive of Avi's former built-in contacts. Current contacts live in the Contacts app, and an app can't call another app's tools. | Invoking project | | `notes:read` | `context.notes.get/list` | Read notes in the invoking project. | Invoking project | | `notes:write` | `context.notes.create/update/delete` | Create, edit, or delete notes in the invoking project. | Invoking project | | `files:read` | `context.files.get` | Read files in the app's file namespace. | Invoking project + app | | `files:write` | `context.files.put` | Write files in the app's file namespace. | Invoking project + app | | `project:files:read` | `context.projectFiles.list/get/search` | Read and search the project's shared file pool (the Files panel). | Invoking project | | `project:files:write` | `context.projectFiles.put/delete/move/updateMetadata` | Create, edit, move, or delete files in the project's shared file pool. | Invoking project | | `logs:write` | `context.log.info`, `context.log.warn`, `context.log.error` | Emit structured app logs. | Current invocation | | `secrets:read` | `context.secrets.get` | Read a project secret by name. Prefer `secretInputs` for per-caller credentials. | Invoking project | | `project:read` | `context.project.info` | Read metadata for the invoking project. | Invoking project | | `llm:invoke` | `context.llm.complete` | Call an LLM billed to the org. | Invoking org/project | | `user:read` | `context.user` | Read the invoking user's id when the invocation is user-initiated. | Current invocation | | `org:members:read` | `context.org.members` | List the org's members — who a record can be shared with. | Invoking org | | `pages:read` | `context.pages.url` | Read the public URL of one of the app's pages (see [Public pages and sites](#public-pages-and-sites)). | Invoking project + app | | `pages:write` | `context.pages.setSlug/aliasUrl` | Set a page's public path, or mint an extra per-thing link to it. | Invoking project + app | | `sites:read` | `context.sites.url/checkHost` | Read the app's site URLs and check whether a subdomain is free. | Invoking project + app | | `sites:write` | `context.sites.claimHost/publish/confirm/remove` | Claim a subdomain, and publish or take down a site's files. | Invoking project + app | | `webhooks:read` | `context.webhooks.url` | Read this install's public URL for a declared webhook (see [Webhooks](#webhooks)). | Invoking project + app | A few more scopes (sending mail and messages as the project, charging credits) are reserved for apps made by Avi. Project-scoped helpers always use the project that invoked the tool. The only cross-project data surface is a store declared `scope: "org"` (one shared partition across the org's enabled projects); every other helper — files, tasks, contacts, notes, updates, events — stays bound to the invoking project. ### Project files vs. app files There are two file surfaces, and they don't overlap: - **`context.files`** (`files:read` / `files:write`) is the app's **private namespace** — a scratch area keyed by the app, invisible in the Files panel. Good for caches, cursors, and internal artifacts. - **`context.projectFiles`** (`project:files:read` / `project:files:write`) is the **project's shared file pool** — the same files the user sees in the Files panel, chat attachments land in, and the Avi agent reads and writes. ```ts // list a folder (or everything), filter by metadata const { data, total } = await context.projectFiles.list({ dir: "reports/", limit: 50 }); // read a file — text comes back utf-8, binary comes back base64 (check `encoding`) const file = await context.projectFiles.get("reports/q2-summary.md"); // full-text + semantic search across the pool const { data: hits } = await context.projectFiles.search({ query: "quarterly revenue", limit: 10 }); // write (creates folders as needed; 10 MB max per file) await context.projectFiles.put({ path: "reports/q3-summary.md", data: "# Q3\n...", contentType: "text/markdown", metadata: { source: "crm-sync" }, }); // single-file move/rename, delete, metadata patch await context.projectFiles.move({ from: "reports/q3-summary.md", to: "archive/q3-summary.md" }); await context.projectFiles.delete("archive/q3-summary.md"); await context.projectFiles.updateMetadata({ path: "reports/q2-summary.md", patch: { reviewed: true } }); ``` Writes show up in the Drive app immediately and are searchable by the agent. `move` handles single files only — it refuses directory paths. User-owned projects install apps individually, and each project controls its own tool permissions. ## Documentation Every app documents itself in a `docs/` directory next to `app.ts` — flat, kebab-case markdown files, uploaded automatically with every `avi deploy` so they can never go stale. `docs/README.md` is the entry page and is required whenever the directory exists; **publishing an app publicly requires it**. ``` my-app/ app.ts docs/ README.md ← what the app does for you, short and friendly integrations.md ← optional deep dives, any kebab-case name troubleshooting.md ``` Avi composes each app's **intro page** — its landing page in the app directory and its Docs tab — from three parts: a generated header (title, summary, version, last deployed, capabilities), your README body, and generated sections listing every tool, setting, webhook, and data store straight from your manifest. **Docs are for people.** Keep the README short and friendly: what the app does for you, getting started, what runs automatically, and good to know (limits, and the moments a person has to step in). Add a page only where a task needs real steps, like a setup walkthrough or troubleshooting. Internals such as architecture, data shapes, and event names stay with your code. Because the intro sections are generated, **never restate manifest content in docs**: no hand-written tool or settings listings, and no app-level `description` in `defineApp`. When you name a tool, use its runtime name (`<app>_<tool>`). **Agents learn your app from your tools, not your docs.** Tool `description`s are the agent's whole contract for calling each tool, so put everything an agent needs there. The agent's project search does index your authored pages, so an agent may come across them while searching. That's one more reason to keep them true. Rules the CLI enforces at deploy: README exists when `docs/` does; slugs are kebab-case and unique; relative links between pages resolve; at most 50 pages, 200 KB per page, 1 MB total. The README takes no frontmatter (its title is the app's title, its one-liner is the manifest `summary`) and starts directly at prose with `##` sections. Deep-dive pages may set `title`, `summary`, and `order` in frontmatter. Images must be `https:` URLs. Once deployed, your docs show on the app's Docs tab in every project that enables it. Treat stale docs as live bugs, not cosmetic debt. ## Build and deploy ```bash avi build avi deploy ``` The CLI bundles your code, uploads it, and makes it available to any project in your org that enables the app. Avi supplies the runtime around your bundle, so improvements to the `context` helpers reach your app without a rebuild. You can pass app-level env vars during deploy; your code reads them from `process.env`: ```bash avi deploy --env BASE_URL=https://api.internal.example ``` `--env` repeats, `--env-file <path>` reads a `.env`-style file, and `--unset-env KEY` removes a stored key. `--org <orgId>` deploys to a different org for this one run, without changing the org you picked at sign-in. ## Iterating Make changes locally, run `avi deploy` again. Check state any time with: ```bash avi status ``` ### Reading your app's logs ```bash avi logs ``` Shows the last hour of your deployed app's logs: everything it printed with `console.*` or `context.log`, each request's start and finish, and faults such as timeouts and crashes. Useful variations: ```bash avi logs --errors --since 6h avi logs --tool my-tool --since 2d avi logs --request <request-id> avi logs --follow ``` - `--since` / `--start` / `--end` take durations (`15m`, `2h`, `7d`) or ISO timestamps. One query covers up to 7 days, and logs are kept for 30 days. - `--errors` shows only failures: your `context.log.error` / `console.error` lines plus timeouts and crashes. - `--tool <name>` shows one tool's invocations; `--request <id>` traces one invocation (use the request id shown on its log lines). - `--search <text>` matches free text; `--level`, `--source`, and `--project` narrow further. - `--follow` tails live while you exercise the app. - `--json` prints one JSON object per line, for piping into other tools (or an agent). The last line is a `{"page": {...}}` object with the queried range and, when the limit was hit, the `nextEndTime` to pass as `--end` (keep your original `--start`) to page back. `avi logs` reads the app from the `app.json` that `avi deploy` writes in the app directory; `--id` and `--org` override it. You only ever see your own org's activity, and crash and timeout lines only show to the org that owns the app. ## Going further Grow the starter from here: [App React UI](/docs/app-react-ui) covers panels and the component library, and [Voice in App Panels](/docs/app-voice) covers what people can say to them. The authoring skill that `avi init` writes into your project carries the full contract, so a coding agent can help you build the rest. --- <!-- Source: https://avi.run/docs/app-react-ui --> # App React UI Apps can ship React panels that open from the rail in Avi (the dock on a phone). Use panels when an app needs a focused UI for browsing, editing, or reviewing the same data its tools use. Panels are not loaded into Avi's main React tree. Avi builds each panel as a browser bundle, serves it inside a sandboxed iframe, and exposes a small bridge for data access, tool calls, theme tokens, and notifications. ## When to use a panel Add a React panel when users need to: - inspect app-owned records, queues, dashboards, or sync status; - edit structured data with controls that are awkward through chat alone; - run an app-specific workflow repeatedly; - share state between the UI and the agent's tools. Keep the agent-facing behavior in tools. The panel is the human interface; tools are still what the agent calls during chat and tasks. ## Project shape `avi init` creates a starter app with a panel: ```text my-app/ app.ts ui/ Dashboard.tsx package.json tsconfig.json ``` The panel entry lives under `ui/` by convention, but any relative path inside the app directory is allowed. ## Declare panels in app.ts Declare UI panels on the same `defineApp` call as your tools: ```ts import { defineApp, type AppToolContextForCapabilities } from "@avihq/apps-sdk"; const capabilities = ["data:read", "data:write"] as const; const data = { customers: { type: "collection", fields: { name: { type: "string", required: true }, status: { type: "enum", enum: ["lead", "active", "at-risk", "churned"] }, }, fullText: ["name"], }, } as const; export default defineApp({ name: "customers", title: "Customers", summary: "Customer records with chat tools and a panel.", icon: "./icon.svg", capabilities, data, ui: { panels: { dashboard: { title: "Customers", icon: "users", entry: "./ui/CustomersPanel.tsx", }, }, }, tools: { "list-customers": { description: "List customer records.", inputSchema: { type: "object", properties: {}, additionalProperties: false, }, outputSchema: { type: "object", properties: { customers: { type: "array", items: { type: "object", additionalProperties: true } } }, required: ["customers"], }, async handler(_input, context: AppToolContextForCapabilities<typeof capabilities, typeof data>) { return { customers: await context.data.customers.list(), }; }, }, }, }); ``` Panel ids are the keys under `ui.panels`. They must be kebab-case, just like app and tool names. ## Panel definition Each panel has: | Field | Required | Notes | |---|---:|---| | `title` | Yes | The panel's name, shown in its frame header. Maximum 60 characters. | | `icon` | No | A built-in icon name, a relative image path, an HTTPS image URL, or a small `data:image/...` URI. Defaults to `panel-right`. | | `entry` | Yes | Relative `.tsx`, `.ts`, `.jsx`, or `.js` path inside the app directory. | | `subscribeEvents` | No | Up to 5 event-type patterns (exact types or a `prefix.*` glob, e.g. `"slack.message.*"`). See [Live updates](#live-updates). | Built-in icons: ```text bar-chart, calendar, chart, clipboard-list, database, folder, gauge, grid, inbox, layout-dashboard, list, mail, notebook, panel-right, settings, table, users ``` Custom icons are rendered as 16 px images. Use a relative SVG, PNG, JPEG, GIF, or WebP path such as `./ui/icon.svg` to have the CLI bundle it into the deployed manifest, or use an HTTPS image URL for a remotely hosted icon. An app can define up to five panels. Prefer one, with extra surfaces as tabs inside it. ## Write the React component Panel code is normal React. Export a default component from the entry file. UI components come from Avi's component library, `@avihq/ui` — import them from its `/panel` entry so the library's stylesheet loads inside the sandboxed iframe automatically — and bridge hooks come from `@avihq/apps-sdk/react`: ```tsx import { useState } from "react"; import { AppShell, Button, EmptyState, Input, Spinner, Toolbar } from "@avihq/ui/panel"; import { DataTable } from "@avihq/ui/data-table"; import { useAvi, useLiveData } from "@avihq/apps-sdk/react"; interface Customer { name: string; status?: string; } export default function CustomersPanel() { const avi = useAvi(); const [customers, setCustomers] = useState<Array<{ id: string; record: Customer }>>([]); const [loading, setLoading] = useState(true); const [name, setName] = useState(""); // Loads on mount and re-runs automatically whenever the store changes — // `customers` is the named collection store declared in app.ts. useLiveData(async () => { const { records } = await avi.data.customers.list(); setCustomers(records); setLoading(false); }, { stores: ["customers"] }); async function save() { await avi.data.customers.create({ name, status: "lead" }); await avi.toast.show({ type: "success", message: "Customer saved" }); setName(""); } return ( <AppShell breadcrumbs={{ items: [{ label: "Customers" }] }} toolbar={ <Toolbar> <Input value={name} onChange={(event) => setName(event.currentTarget.value)} /> <Button onClick={save} disabled={!name.trim()}>Save</Button> </Toolbar> } padded={false} > {loading ? ( <EmptyState title="Loading" icon={<Spinner />} /> ) : customers.length === 0 ? ( <EmptyState title="No customers" /> ) : ( <DataTable columns={[ { accessorKey: "record.name", header: "Name" }, { accessorKey: "record.status", header: "Status" }, ]} data={customers} flush getRowId={(row) => row.id} /> )} </AppShell> ); } ``` The CLI wraps your component with React's `createRoot`, so the entry file should export the component rather than calling `createRoot` itself. ## The Avi component library (`@avihq/ui`) `@avihq/ui` is the same component library Avi's own dashboard and built-in apps use, so panels built with it look like an official part of the product. Every component is styled through Avi's design tokens and follows the host theme (dark/light) automatically. It ships with the starter project; this page assumes version 0.3 or later — if your app's `package.json` lists an older `@avihq/ui`, run `npm install @avihq/ui@^0.3.0`. Import from `@avihq/ui/panel` in panel code (the bare package root is for hosts with their own CSS pipeline and won't inject styles into the iframe). | Group | Components | |---|---| | Layout | `AppShell` (the standard panel container), `SplitView` (adaptive master-detail), `ActionBar` (pinned actions), `Band` (the standard bar — use it for every title or option bar of your own), `Toolbar`, `EmptyState` | | Form controls | `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, `RadioGroup`, `Slider`, `TagInput`, `DatePicker`, `DateRangePicker`, `ToggleGroup` | | Feedback | `Alert`, `Badge`, `DraftBadge`, `Menu`, `Modal` (with `ModalHeader`, `ModalBody`, `ModalActions`), `ConfirmDialog`, `Tooltip`, `InfoTip`, `Spinner` | | Navigation | `Tabs` (with `Tabs.Tab` and `Tabs.Panel`), `Breadcrumbs` | | Data display | `Card`, `Table` (with `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`), `Heading`, `Text`, `Divider`, `CodeBlock` (syntax-highlighted), `Markdown` (read-only GFM renderer) | | Records | `RecordPage`, `RecordHeading`, `DetailRow`, `LoadingPane`, `DeleteArmedButton`, `ReplyContext`, `FilterControl`, `useSortVoice`, `replaceListingWindow`, and the `useLiveRecord` hook — see [Records: lists, record views, and editing](#records-lists-record-views-and-editing) | | Hooks | `useSurface`, `useVoiceTargets`, `useVoiceScreen` | `InfoTip` needs a release of `@avihq/ui` later than 0.3.0. ### AppShell — the standard panel container Structure every panel as an `AppShell`. Chrome rows — breadcrumbs, tabs, toolbar — are slots, so they render full-bleed at the top in the right order, and body padding can never push them off the panel edges. The body below is the panel's single scroller, padded by default: ```tsx <AppShell breadcrumbs={{ items: [{ label: "Deals", onClick: home }, { label: deal.title }] }} tabs={{ tabs: [{ value: "details", label: "Details" }, { value: "notes", label: "Notes" }], value: tab, onChange: setTab }} > {tab === "details" ? <Details /> : <Notes />} </AppShell> ``` Pass `padded={false}` for full-bleed tables and lists, `scrollable={false}` when the body manages its own panes (SplitView). AppShell also scopes `Modal` dialogs to the panel automatically — no `container` wiring — and wraps the body in an error boundary, so a render crash shows a themed fallback instead of a blank panel. Tab-row actions go through the tabs slot's `action` and `menu` fields rather than custom elements in the row. On mobile the row shows navigation plus at most one kebab (the action folds in as its first item). On the sidebar the action shows inline while the row is at least 480px wide — the standard row breakpoint, shared with Breadcrumbs' `collapseBelow` — and folds into the kebab below that; the measurement is container width, so a row inside a SplitView pane folds by its pane. On fullscreen the action is always inline, compact, never touching the row borders. **Breakpoints within a surface.** The shell and each SplitView pane are CSS size containers, so your own markup can step by container width with Tailwind container-query variants — `@md:grid-cols-2`, `@min-[480px]:flex-row`. Surfaces decide ergonomics (touch vs pointer); container breakpoints decide density within a surface. ### SplitView — adaptive master-detail For list-plus-record panels, render a `SplitView` inside the shell: a `list` pane, a `detail` pane, and `detailOpen` driven by your selection state (usually a `useAviView` record id). It adapts to the surface automatically — side-by-side panes on fullscreen, a state-preserving drill-in on the sidebar and mobile where the open record covers the list (the list keeps its scroll position; backing out is instant). Give the detail a `Breadcrumbs` whose root part is the back affordance, and supply an `EmptyState` for fullscreen's nothing-selected case. ### ActionBar — pinned actions Record-level actions (Reply on a thread, Save on an editor, Delete) go in an `ActionBar` — AppShell takes it as an `actionBar` slot, and inside a SplitView detail pane you render it after the pane's scroller. The bar is always the LAST row of its pane, on every surface, and casts the band shadow upward over the content scrolling past it (a hand-placed bar at the top of a pane can flip that with `placement="top"`). Structured props (`action`, `secondaryAction`, `utilities` with icons) render by surface: compact icons-left/actions-right on sidebar and fullscreen; on mobile the bar grows to touch height and the primary action stretches across the remaining width. A `children` escape hatch keeps the chrome for flows the config can't express. ### Surfaces — adapting to where the panel is mounted The host stamps `data-avi-surface` on the panel document: `sidebar` (desktop side panel — dense, pointer-driven, the default), `mobile` (the native app or mobile web — touch ergonomics), or `fullscreen`. The library's components adapt on their own (touch-size tabs on mobile, side-by-side SplitView on fullscreen). For your own markup, use the `mobile:`/`fullscreen:` Tailwind variants (`text-sm mobile:text-[15px]`), and reach for the `useSurface()` hook only when the DOM shape itself must change. Never branch layout on viewport width — a 400px desktop sidebar and a phone are the same width but need different ergonomics. `PanelShell`, `PanelBody`, and `PanelHeader` remain for panels that predate `AppShell`; new panels should not use them. ### Entry points Heavier building blocks ship as separate entry points so your panel only bundles what it imports. They don't load the library's stylesheet themselves — it comes from `@avihq/ui/panel`, which every panel already imports for `AppShell`: | Entry | What you get | |---|---| | `@avihq/ui/charts` | Themed chart components on recharts — `LineChart`, `BarChart`, `AreaChart`, `PieChart`, `ScatterChart`, `RadarChart`, `ComposedChart` | | `@avihq/ui/data-table` | `DataTable` — table with sorting, filtering, and pagination, plus row virtualization for large datasets (`virtualized`); pass `flush` when it sits full-bleed in a panel so its container border doesn't double up — and `RecordBrowser`, the composed list-plus-record surface built on it | | `@avihq/ui/editor` | `Editor` — Avi's rich markdown editor (toolbar, GFM, markdown strings in/out) | Charts are heavy (~480 KB minified) — keep the 4 MB panel bundle limit in mind. ### Records: lists, record views, and editing Most panels are some form of "browse records, open one, act on it." The library ships that whole pattern as components, so you compose instead of rebuilding it — and everything you compose from them is automatically [voice-operable](/docs/app-voice). #### RecordBrowser — browse records, open one `RecordBrowser` (from `@avihq/ui/data-table`) is the complete list-plus-record surface: a `SplitView` that shows panes side by side on fullscreen and drills in on the sidebar and mobile, a `DataTable` list with infinite scroll (pass `virtualized` for long lists), and a built-in record header — breadcrumbs whose root closes the record, an optional actions slot, and a close button. Selection is controlled: you pass `openId` and `onOpenChange` (usually backed by `useAviView`), and the browser renders whatever you say is open. ```tsx import { RecordBrowser } from "@avihq/ui/data-table"; <RecordBrowser title="Invoices" records={invoices} columns={[ { accessorKey: "vendor", header: "Vendor", narrow: "title" }, { accessorKey: "amount", header: "Amount", narrow: "meta" }, ]} getRecordId={(invoice) => invoice.id} openId={openId} onOpenChange={setOpenId} detail={openId ? <InvoiceDetail id={openId} /> : null} detailTitle={openInvoice?.vendor} voice={{ noun: "invoice", recordLabel: (invoice) => invoice.vendor, recordPhrases: (invoice) => [invoice.number], }} /> ``` What you get for free: narrow panes adapt by column `priority` (columns that don't fit are dropped, never squished) or by `narrow` slots (`"title"`, `"meta"`, `"line"` — the list becomes one composed row per record that STACKS vertically: the title on its own line, the meta cells on one secondary line below it in column order, each `line` column full-width under that; the object form `narrow: { slot, cell }` swaps in a narrow-specific cell, e.g. a title that wraps to two lines); `loading`, `emptyTitle`/`emptyDescription`, and `onEndReached` cover first load, empty, and infinite scroll; `listHeader` is the standard toolbar band for filters and actions; and the `voice` config makes every row spoken-addressable ("open the third invoice"). For lists a flat table can't express (day groups, sections), pass `listContent` and render your own body — the browser still owns the panes, the record header, and selection. Use `RecordBrowser` for anything list-shaped with an open-record state. Reach for raw `SplitView` + `DataTable` only when the pattern genuinely doesn't fit. #### FilterControl — the standard list filters `FilterControl` renders the one compact Filter button with a dropdown over declared filter groups — single-select (with an optional "All" row via `emptyLabel`), multi-select, and date ranges — plus an active-count badge and a clear-all row. Declare the groups as data and handle one callback: ```tsx import { FilterControl } from "@avihq/ui/panel"; <FilterControl groups={[{ id: "status", label: "Status", kind: "select", emptyLabel: "All", options: [{ id: "open", label: "Open" }, { id: "done", label: "Done" }], value: status, }]} onChange={(groupId, value) => setStatus(value as string | null)} /> ``` Put it in a list's toolbar (`RecordBrowser`'s `listHeader` slot is the standard spot). Use one `FilterControl` per view — it also carries the view's complete spoken filter surface ("only open", "clear the filters", "filter from June 1 to June 15") with no extra code. A `multi-select` filter group adds a spoken pick to its selection by default. Set `voicePicksOne: true` on a group whose resting selection isn't empty, such as a task list that shows two unfinished statuses by default. Then "only complete" selects exactly that option instead of widening the list. Clicking still toggles each checkbox. #### Sorting — the header row is the control Lists sort by clicking a column header (`sortable`); there is no separate Sort button. Give the list a default (newest activity first is what Avi's own apps use) and keep the state controlled so header clicks, the persisted preference, and spoken sorts are one state: ```tsx import { RecordBrowser, type SortState } from "@avihq/ui/data-table"; const DEFAULT: SortState = { id: "updated", desc: true }; const [sort, setSort] = useState<SortState>(DEFAULT); <RecordBrowser sortable sorting={[sort]} onSortingChange={(next) => setSort(next[0] ?? DEFAULT)} voiceSorts={[ { id: "updated", label: "updated", aliases: ["date"], flavor: "date" }, { id: "state", label: "state", directionLabels: ["Open first", "Closed first"] }, { id: "title", label: "title", aliases: ["name"], flavor: "text" }, ]} … /> ``` Each sortable column needs an `accessorFn` (or `accessorKey`). `voiceSorts` on `RecordBrowser` speaks the sorts ("sort by updated", "newest first", "sort by name", "a to z", "closed first", and the aliases you declare) and drives the same controlled state, which is what keeps sorting alive in a narrow pane: the stacked rows have no header cells, so `RecordBrowser` orders the records itself from the columns' accessors under that state. Persist the value like any list preference (a KV key per list). Keep aliases to two per sort: every phrase past the per-target cap becomes a published shard, and shards take manifest room from rows. #### Fields speak by default Every form field in the library answers to speech with no wiring: `Input`, `Textarea`, `Select`, `DatePicker`, `Checkbox`, and `Switch`. A field is spoken of by the name it already shows: its `label`, else its `aria-label`, else a placeholder you set. So `<Input label="Title" … />` answers "the title is Q3 plan", "set the title to …", "type in the title …", and "clear the title". A field with no name at all can't be spoken to, so give an unlabeled field an `aria-label`. - `voice={{ id, label }}` names the field yourself: a stable id for the agent (`field:<id>`) and the words people use ("the comment"). - `voice={false}` opts a field out. - Secrets never speak: passwords, card numbers, one-time codes, and hidden or file inputs register nothing, and neither do disabled or read-only fields. Only the field's name is published, never its value. - "the <name> is …" and "set the <name> to …" replace the value; "type in the <name> …" adds to it. - Checkboxes and switches answer "turn on <name>", "turn off <name>", "check …", "uncheck …", and "toggle <name>", through your own `onChange`. **Inside a dialog**, the focused field (or the dialog's only field) also takes speech that matches nothing else, so a person can just say the name a dialog asks for: "Acme engagement letter". It fills the field and never submits. Questions and anything addressed to Avi ("Avi, …") still go to the agent. Give a dialog its own extra phrases with `<Modal voiceTargets={[…]}>`: a `useVoiceTargets` call in the component that renders the `Modal` sits outside the dialog and is silenced while it is open. #### Dictation into fields — `voice` on Textarea and Input A text field that declares `voice={{ id, label }}` (or is named by its label, as above) takes speech: "type in the comment looks good to me" appends to it (the field's own label always names it; while the field is FOCUSED the bare "type …", "write …", "dictate …", "enter …" reach it too, so a modal's single field takes plain dictation), and "clear the comment" empties it. The agent types into the same field through the same door, addressing it as `field:<id>` (or by its label) — so when your view opens a composer, publish a `useVoiceScreen` note (300 characters at most) that leads with typing into `field:<id>` and says never to post around the composer with a data tool; the agent will otherwise reach for the data tool while the user is still dictating. Values land through the element's native value setter plus an `input` event, so your controlled `onChange` sees them exactly like typing. ```tsx <Textarea autoFocus value={body} onChange={(e) => setBody(e.target.value)} voice={{ id: "comment-body", label: "the comment" }} /> ``` #### Choosing and dating by voice — `voice` on Select and DatePicker A `Select` and a `DatePicker` speak by their label by default; the same `voice={{ id, label }}` prop names them yourself, and the phrases are the pointer's own moves, so nothing new has to be wired: - `Select` — "set the status to completed", "change status to open", "update the status to cancelled", "make status completed" pick an option through your `onChange`; "change the status" / "update status" / "edit the status" opens the list, and while it is open the bare option labels ("completed") pick and "close" dismisses — everything outside the list goes quiet meanwhile, exactly like a modal. The agent's door is `field:<id>@<option label or value>`. When an option's spoken name differs from its label, give it `voiceAliases` — an option `{ value: "4", label: "Urgent", voiceAliases: ["4", "four"] }` answers "set priority to 4", "set priority to four", and "set priority to urgent". - `DatePicker` — "set the due date to next friday", "change due date to september 12th", "update the due date to tomorrow", "move due date to the 15th" resolve the spoken date (weekdays, month-and-day, "the 15th", "in two weeks", "end of the month", ISO, m/d) and call `onChange` with the ISO day; a date it cannot read opens the calendar instead, so a miss is visible. "clear the due date" / "no due date" empties it; bare "change the due date" opens the calendar. Add `aliases` for the words your domain uses ("due", "postpone to") — each becomes another template. The agent's door is `field:<id>@<spoken date or ISO>`. ```tsx <Select label="Status" options={STATUS_OPTIONS} value={status} onChange={setStatus} voice={{ id: "status", label: "the status" }} /> <DatePicker label="Due date" value={due} onChange={setDue} voice={{ id: "due-date", label: "the due date", aliases: ["due", "postpone to"] }} /> ``` One rule keeps a form and a list filter from colliding on the same screen: **fields own the mutation verbs** (set / change / update / make / switch / move) and **filters own the filter verbs** ("filter by completed", "only open", "status completed", "filter status to completed"). A `FilterControl` never claims "set status to completed" — that phrase belongs to the open record's Status field. #### Full-area records — `detailFills` Some records deserve the whole surface: a pull request's side-by-side diff reads badly beside a list column. `RecordBrowser detailFills` (SplitView's prop of the same name) gives the open record the entire area on every surface — the list unmounts until the crumb root, the close X, or a spoken "close" brings it back. Closing re-renders the list from your records, but its scroll position starts over. That's deliberate: the hidden list's rows and filters drop out of the voice targets, so they never crowd out the open record's own. Nothing else changes: the record header band, crumbs, and voice dismiss target are the same ones the side-by-side layout uses. ```tsx <RecordBrowser detailFills … /> ``` #### RecordPage, RecordHeading, DetailRow, LoadingPane — the record view An open record renders as a full view built from the record shells: - `RecordPage` — the one record-page frame: a breadcrumbs row (root part navigates back), an optional pinned `heading` band, the scrolling body, and an optional pinned actions row. It also owns the loading and load-error states (spinner, then an error panel with Retry) so you only render the loaded case. - `RecordHeading` — the title block: full title, an optional muted meta line ("From Dana - Yesterday"), and a trailing actions slot. - `DetailRow` — labeled rows on a shared label column, so values line up ("To", "Subject", "Due"). - `LoadingPane` — the centered spinner filler for any pane that's still loading. ```tsx <RecordPage root="Invoices" onBack={() => setOpenId(null)} title={invoice?.vendor ?? ""} loaded={invoice != null} loadError={error} errorTitle="Couldn't load this invoice" loadingTitle="Loading" onRetry={reload} heading={invoice && <RecordHeading title={invoice.vendor} meta={invoice.dueDate} />} actions={<Button onClick={markPaid}>Mark paid</Button>} > {invoice && ( <> <DetailRow label="Amount">{invoice.amount}</DetailRow> <DetailRow label="Status">{invoice.status}</DetailRow> </> )} </RecordPage> ``` Inside a `RecordBrowser` detail pane, pass `hideCrumbs` — the browser renders its own record header. #### useLiveRecord + useRecordRefresh — the live record view Two hooks make an open record stay correct while the world changes under it — the agent edits the same draft, a sync updates the record, another device deletes it: - `useLiveRecord` (from `@avihq/ui/panel`) owns the record lifecycle. You supply `fetch` plus two callbacks: `onLiveApply` (apply fresh data to the form — only called while it's safe, never over the user's own un-saved typing) and `onGone` (the record vanished — navigate away, or offer to keep unsaved work). It returns the loading/error state, `refresh` (silent, change-detected), `reload` (hard, with a spinner), editing/touched markers for your form handlers, and `beginSelfMutation` for wrapping your own delete/send so it isn't reported back to you as "the record vanished". - `useRecordRefresh` (from `@avihq/apps-sdk/react`) wires every standard refresh nudge in one call: changes to the data stores you list in `stores`, the live events you name in `events` (names or a predicate), the tab becoming visible again, a reconnect, and a slow safety poll (every 30 seconds while visible). Only listed stores and events count. ```tsx const live = useLiveRecord<Invoice>({ id: invoiceId, fetch: async () => { const r = await avi.apps.invoke("billing_read-invoice", { invoice_id: invoiceId }); return r.deleted ? { record: null, goneReason: "deleted" } : { record: r.invoice }; }, onLiveApply: (invoice) => setFields(fieldsOf(invoice)), onGone: ({ reason }) => setOpenId(null), }); useRecordRefresh(() => void live.refresh(), { stores: ["invoices"] }); ``` Never hand-roll `setInterval` pollers or visibility listeners — this pair replaces all of it, and refreshes arrive already debounced. #### ConfirmDialog and DeleteArmedButton — destructive actions `ConfirmDialog` is the standard destructive confirmation: one question that carries the verb ("Delete invoice?"), one Confirm button, one way out. `DeleteArmedButton` composes it into the standard two-step trash button — click to arm, confirm to act. Its `label` names the button and becomes the dialog's question: ```tsx <DeleteArmedButton label="Delete invoice" busyLabel="Deleting…" busy={deleting} onConfirm={deleteInvoice} /> ``` Render the same delete affordance in **every** mode of a view (read and edit) — and always route destructive actions through the dialog rather than acting on first click. #### ReplyContext — the reply-draft convention When a draft replies to something (an email, a message, a comment), render the original read-only *below* the draft inside `ReplyContext` — a labeled band (`label`, default "In reply to", with an optional muted `meta` beside it — the sender, a date) wrapping your own read-only rendering. Every app with reply-style drafts uses the same shape, so users always know where the original is. One layout rule: the band never shrinks, so every sibling in the same scrolling column must be unshrinkable too (`flex: "1 0 auto"` for the growing region, `flexShrink: 0` for the rest) — otherwise the band squeezes your draft to zero height instead of the page scrolling. Using the library is encouraged, not required — regular React with your own styling works too (see [Theme and styling](#theme-and-styling) for the token variables to use). The older UI primitives exported from `@avihq/apps-sdk/react` (`Button`, `Input`, `Badge`, …) still render for already-deployed apps, but new panels should use `@avihq/ui/panel`. ### Coloring code line by line `CodeBlock` colors a whole string. For a reading that draws its own rows — a diff laid into a file, a side-by-side view — use the line highlighter: ```tsx import { highlightLines, languageForFilename, HighlightedLine } from "@avihq/ui/panel"; const lines = highlightLines(text, languageForFilename(path)); // one token list per line <td><HighlightedLine tokens={lines[n - 1]} /></td> ``` The text is tokenized whole, so block comments and template strings stay right across lines. Colors are the `--color-syntax-*` theme tokens (light and dark), so highlighted text reads on the panel's own background. Unknown languages come back as plain lines. ## SDK React exports Hooks: | Export | Purpose | |---|---| | `useAvi()` | Returns the full panel bridge. | | `useTheme()` | Returns `{ mode, tokens }` for the current Avi theme. | | `useAviView(defaultView?)` | Persistent panel view — see [Persisting the panel's view](#persisting-the-panels-view). | | `useLiveData(loader, options?)` | Run a loader on mount and re-run it on live events — see [Live updates](#live-updates). | | `useLiveEvents(handler)` | The raw stream of the app's own live events — see [Live updates](#live-updates). | | `useAviEvents(handler)` | Nudges for declared project activity — see [Live updates](#live-updates). | | `useRecordRefresh(refresh, options?)` | Every standard record-refresh nudge in one call — see [Records](#records-lists-record-views-and-editing). | | `useAppData()` | Shortcut for `useAvi().data` — the named data stores. | | `useAppRecords()` | **Deprecated** alias of `useAppData()`. Record collections are now declared data stores; reach them via `useAppData().<storeName>`. | | `useProjectUpdates()` | Shortcut for `useAvi().updates`. | | `useProjectTasks()` | Shortcut for `useAvi().tasks` — the read-only archive of Avi's former built-in tasks (tasks now live in the Tasks app). | | `useProjectContacts()` | Shortcut for `useAvi().contacts` — the read-only archive of Avi's former built-in contacts (contacts now live in the Contacts app). | | `useProjectNotes()` | Shortcut for `useAvi().notes`. | | `useProjectFiles()` | Shortcut for `useAvi().projectFiles` — the project's shared files. | | `useAppTool()` | Shortcut for `useAvi().apps.invoke`. | ## Bridge API Panels cannot call Avi APIs directly. Use `useAvi()` to make bridge calls through the parent app: ```ts const avi = useAvi(); ``` Available bridge methods: Data goes through the app's **named stores**, keyed by the names declared in `defineApp({ data })`. `avi.data.<store>` is a KV-store handle when the store is `type: "kv"` and a record-collection handle when it is `type: "collection"` (the bridge routes the call from the store→type map injected into the panel frame). `avi.records` is removed. | API | Capability | What it does | |---|---|---| | `avi.data.<kvStore>.get(key)` | `data:read` | Read a value from a declared KV store. | | `avi.data.<kvStore>.has(key)` | `data:read` | Check a key in a declared KV store. | | `avi.data.<kvStore>.listKeys(options)` | `data:read` | List keys in a declared KV store. | | `avi.data.<kvStore>.set(key, value)` | `data:write` | Write a value to a declared KV store. | | `avi.data.<kvStore>.delete(key)` | `data:write` | Delete a key from a declared KV store. | | `avi.data.<collection>.get/list/search` | `data:read` | Read records from a declared collection store and run full-text/vector search. | | `avi.data.<collection>.create/update/delete` | `data:write` | Write records in a declared collection store. | | `avi.updates.list(options)` | `updates:read` | Read project timeline updates. (Updates are published from the app's `handler` via `context.updates.publish`, not from panels.) | | `avi.tasks.get/list` | `tasks:read` | Read the read-only archive of Avi's former built-in tasks (tasks now live in the Tasks app). | | `avi.contacts.get/list` | `contacts:read` | Read the read-only archive of Avi's former built-in contacts (contacts now live in the Contacts app). | | `avi.notes.get/list` | `notes:read` | Read notes in the invoking project. | | `avi.notes.create/update/delete` | `notes:write` | Create, edit, or delete notes in the invoking project. | | `avi.files.get(key)` | `files:read` | Read a project/app file as string data. | | `avi.files.put(key, blob)` | `files:write` | Write a project/app file. | | `avi.projectFiles.list/get/search` | `project:files:read` | Read and search the project's shared file pool (the Files panel). | | `avi.projectFiles.put/delete/move/updateMetadata` | `project:files:write` | Create, edit, move, or delete files in the project's shared file pool. | | `avi.apps.invoke(tool, input)` | None | Invoke one of this app's own tools. | | `avi.user.current()` | None | Return the current user id, or `null`. | | `avi.toast.show(input)` | None | Show an Avi toast in the parent app. | The bridge enforces the same approved capabilities as tool handlers. If a panel calls `avi.data.<store>.set` but the app was not approved for `data:write`, the call fails. Bridge values must be JSON-compatible except file blobs, which use: ```ts { data: string; contentType?: string; } ``` ## Shared state with tools Panels and tools read and write the **same named data stores**. The panel reaches a store through `avi.data.<storeName>` and a tool reaches the same store through `context.data.<storeName>` — for app-like data, prefer a `type: "collection"` store so both sides share CRUD, filters, full-text search, and vector search. For example, a panel can write to the declared `customers` collection store: ```ts await avi.data.customers.create({ name: "Northstar Labs", status: "active", notes: "Expansion candidate", }); ``` Then a tool in the same app can read it: ```ts const customers = await context.data.customers.search({ text: "expansion", vector: "accounts likely to expand", }); ``` Each store's scope (declared per store in `defineApp({ data })`) decides isolation: a `"project"` store is isolated by project and app; an `"org"` store is isolated by org and app and shared across the org's enabled projects. ## Invoking tools from a panel Use `avi.apps.invoke` only when the panel needs behavior already exposed as a tool: ```ts const result = await avi.apps.invoke("customers_list-customers", { // this app is "customers" status: "active", }); ``` Tool names use Avi's runtime format: ```text <app-name>_<tool-name> ``` The tool must belong to this app — a panel may only invoke its own app's tools. (Cross-app invocation was removed.) Tools default to callable from the agent, panels, and the app's own background handler. To keep a panel helper out of the agent's tool list, mark it as UI-only in `app.ts`: ```ts tools: { "refresh-panel-data": { description: "Refresh dashboard data for the React panel.", inputSchema: { type: "object", additionalProperties: false }, outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] }, callableFrom: "ui", async handler() { return { ok: true }; }, }, } ``` Do not use tool invocation as a substitute for simple data reads and writes. Prefer `avi.data` for panel-local CRUD and keep tool invocation for reusable business logic. ## Theme and styling Avi sends theme tokens into the iframe and keeps them updated when the user changes theme. **Tailwind classes work in panel code.** When you deploy, Avi compiles the Tailwind utilities your source uses and ships them inside the panel bundle, so `className` behaves like a normal Tailwind v4 project — layout, spacing, responsive variants (`md:`), and states (`hover:`, `focus:`): ```tsx <div className="flex h-full flex-col"> <div className="flex items-center justify-between gap-2 p-3"> <span className="heading-sm text-txt-primary truncate">Invoices</span> <span className="text-xs text-txt-secondary shrink-0">Synced just now</span> </div> </div> ``` Colors go through Avi's token utilities so panels always match the host theme, dark mode included: `bg-bg-primary`, `bg-bg-secondary`, `text-txt-primary`, `text-txt-secondary`, `text-txt-danger` (also `-success`, `-warning`, `-info`), `border-bdr-primary`, and `heading-xs` through `heading-3xl` for titles. The stock Tailwind palette (`bg-red-500` and friends) is deliberately absent — only `black` and `white` survive. Two rules, both standard Tailwind: write complete class names in string literals (in template literals, keep each branch a full name — `` `${dense ? "gap-1" : "gap-3"}` ``), and never assemble a class name from fragments at runtime — the compiler only ships names it can see whole in your source. The `@avihq/ui` components use Avi's design tokens automatically. For dynamic values Tailwind can't express (measured widths, computed positions), inline styles with the same token CSS variables are the fallback — they are plain color values (use them directly, never wrapped in `hsl()`): ```css var(--color-background-primary) /* main surface */ var(--color-background-secondary) /* subtle surfaces, hovers */ var(--color-text-primary) /* primary text */ var(--color-text-secondary) /* muted text */ var(--color-border-primary) /* default borders */ var(--color-text-danger) /* error text (also -success, -warning, -info) */ var(--border-radius-md) var(--font-mono) ``` ```tsx <div style={{ borderBottom: "1px solid var(--color-border-primary)" }}> <span style={{ color: "var(--color-text-secondary)" }}>Synced just now</span> </div> ``` Use `useTheme()` if the component needs to branch on light or dark mode: ```tsx import { useTheme } from "@avihq/apps-sdk/react"; const theme = useTheme(); const isDark = theme.mode === "dark"; ``` Panels should be compact and task-focused: dense tables, filters, editors, and status summaries usually work better than landing-page layouts. ## Persisting the panel's view Avi remembers which panel each project had open. Your panel's *internal* state — the active tab, the open record — is yours to describe: report it as a **view**, a single opaque string, and Avi mirrors it into the page URL. When the user switches projects and comes back, reloads, or shares the link, your panel reopens exactly where they left it. `useAviView` is a drop-in replacement for `useState` on whatever string names your navigation state: ```tsx import { useAviView } from "@avihq/apps-sdk/react"; // A tab bar: const [view, setView] = useAviView("overview"); // Or an open record: const [selectedId, setSelectedId] = useAviView(); ``` Rules of thumb: - The string is opaque to Avi — encode whatever you want (tab name, record id, `tab/record` path). It is capped at 512 characters. - Always validate restored values: a remembered view can be stale (a deleted record, a renamed tab). Fall back to your default instead of rendering a broken state. - Use it for *navigation* state, not ephemeral UI state (search text, sort order, scroll position). Doing nothing is also fine — a panel that never calls `useAviView` just opens at its default view every time. ## Live updates Panels are sandboxed and can't open their own network connections, so live behavior is host-brokered, and there are two kinds of it: the app's **own live events** (its private backend→panel channel) and **project activity** (declared). ### Live events — the app's own channel Every app has a private live-event stream from its backend to its open panels: arbitrary named events that trigger panel refreshes — or any panel reaction — **for anything**, not just data changes. Two publishers feed it: - **Automatic**: every data-store write publishes `"data.changed"` (with the store name), no matter who wrote — a tool running in chat, another open panel, a background run. On by default; a store opts out with `live: false` in its definition. - **Your backend, for anything else**: `context.live.publish(name)` from any tool or the background handler — an external sync finished, a webhook landed, a long job progressed: ```ts await context.live.publish("sync.finished"); await context.live.publish("import.progress", { scope: "org" }); ``` Names are yours to invent (lowercase dotted segments, max 64 chars; the `data.` namespace is reserved). `scope: "project"` (default) reaches the invoking project's panels; `"org"` reaches the app's panels across the org's projects. Bursts coalesce (~250ms per name), and no capability is needed — live events can only ever reach your own app's panels. On the panel side, `useLiveData` is the simple one-liner — the loader runs on mount and re-runs (debounced) on every live event: ```tsx import { useLiveData, useAvi } from "@avihq/apps-sdk/react"; function CustomersPanel() { const [rows, setRows] = useState([]); const avi = useAvi(); const { refresh, isRefreshing } = useLiveData(async () => { const { records } = await avi.data.customers.list(); setRows(records); }, { stores: ["customers"], // optional: which data changes count events: ["sync.finished"], // optional: which published events count }); } ``` For an open record, `useRecordRefresh` is the standard bundle of nudges — see [Records](#records-lists-record-views-and-editing). `useLiveEvents` is the raw stream, for reactions that aren't refetches: ```tsx useLiveEvents(({ name, store }) => { if (name === "export.ready") setBanner("Your export is ready"); }); ``` Events carry only their name — never payloads; the loader refetches through the capability-checked bridge, exactly like a manual refresh. `refresh()` triggers the loader manually; `isRefreshing` is true while it runs; the loader also re-runs after a WebSocket reconnect. One app's live events never reach another app's panels. **Rail dots are opt-in.** While the panel is closed, only a publish that explicitly asks for attention lights the tile's dot: ```ts await context.live.publish("inbox.updated", { notify: true }); ``` Silent publishes and the automatic `data.changed` events still refresh open panels but never dot — reserve `notify` for moments a human should notice (an export finished, new inbound items arrived), not routine data churn. A `notify` dot is transient: a reload clears it. For attention that should last until it's dealt with (unread items waiting), set the durable badge instead — it persists across reloads until the app clears it: ```ts await context.live.publish("inbox.updated", { badge: true }); // set it from your unread state await context.live.publish("inbox.updated", { badge: false }); // clear it when that state drains ``` Panels' declared `subscribeEvents` matches also light the dot (see Project activity below). Do not add `app.live.event` (the platform's internal wire type) to `subscribeEvents` — your app's live events are delivered automatically, and deploys that declare it are rejected. ### Project activity — declared To react to project activity (Slack messages, task changes, vendor events), declare event patterns on the panel: ```ts panels: { team: { title: "Team", entry: "./ui/TeamPanel.tsx", subscribeEvents: ["slack.message.*"], }, }, ``` Two things happen for a declared pattern: - **In-panel nudges.** While the panel is open, the host forwards each matching project event into the iframe as a *lean nudge* — the event **type only**, never the payload. Treat a nudge as "something changed, refetch through your tools." The host also forwards `"*"` after a WebSocket reconnect: refetch everything, since events may have been missed while offline. - **Rail unread dot.** While the panel is closed, a matching event puts a small dot on the panel's rail tile. Opening the panel clears it. Subscribe with `useAviEvents` from `@avihq/apps-sdk/react` (latest-handler semantics — no memoization needed; never fires outside a panel host): ```tsx import { useAviEvents } from "@avihq/apps-sdk/react"; useAviEvents(({ eventType }) => { // eventType: "slack.message.received", or "*" after a reconnect. refetch(); }); ``` Rules: - Up to 5 patterns per panel; each is an exact event type or a `prefix.*` glob, max 100 characters. - Nudges only fire for events in the project the panel is mounted in. - Debounce your refetches (bursts of events arrive together) and never render from a nudge — always refetch through a tool or data store. ## Voice Panels built with the library are voice-operable automatically: users can open records by name, drive filters and sorts, press declared buttons, and confirm destructive actions entirely by speech, and the agent knows what the panel is showing. The components carry all of it — see [Voice in App Panels](/docs/app-voice) for what users can say and how to make your panel more voice-capable. ### When a list publishes more than the host can carry The host reads at most 150 voice targets from a panel. The registry publishes the dismiss targets first, then the chrome (tabs, crumbs, actions) whole, then the rows. A filter with one option per value (a repo filter over a large account) can be most of that budget on its own, so `FilterControl` marks its per-option targets `deferrable`: they come after the rows and share the rows' cap. Set `deferrable: true` on any chrome of your own that a big list should outrank. ### Naming a pane people scroll by voice The host's scroll grammar knows "the page", "the screen", "the list", "the panel", and "it". A `RecordBrowser` list already answers to its title and row noun ("scroll the invoices down"), so register a named container only for other panes. A pane people call by another name (a file tree, a column) registers as a named container: an item target with noun `column` whose label and phrases are its names, and the pane stamped `data-voice-scroll="column"` with `data-voice-column-id` equal to that target's id. "Scroll the files down", "scroll down in the file tree", "page down in the folders", "to the top of the files" then resolve in the host and the runtime scrolls the pane. Keep the names within the eight-phrase budget, article-free. ## Build and deploy `avi build` bundles `app.ts` into `dist/app.mjs` for Avi's app runtime and validates the panel manifest shape: ids, titles, icons, and entry paths. `avi deploy` rebuilds the app, bundles each panel for the browser sandbox, and uploads the app and its panel bundles. Avi serves the latest panel bundle for the deployed app revision. React ships inside the panel bundle, so declare both `react` and `react-dom` at the same version in your app's `package.json`. The build always bundles a single, version-matched pair — if your install ends up with mismatched copies, `avi deploy` fails with an error telling you which versions to install. **Lazy chunks for heavy code.** Heavy, rarely used code (a document engine, a chart library) can ship as a lazy chunk instead of inside the panel bundle: declare `chunks: { "<id>": "./ui/<entry>.ts" }` on the panel and load it with `await avi.chunks.load("<id>")` when the feature is used. A panel takes up to four chunks of up to 16 MB each, and none of it counts against the panel bundle. Chunks can't import React, the SDK, or the UI library — expose plain functions and DOM, and render them from the panel. Run: ```bash npm run typecheck avi build avi deploy ``` After deploy, enable the app in a project. If the app defines panels, their icons appear on the project's rail. ## Security model Panels run inside an iframe with a restrictive sandbox and content security policy: - the sandbox allows scripts, forms (handled by your own code — a form never submits anywhere), links that open in a new tab, and downloads the user starts, and gives the panel an isolated origin of its own; - panel code cannot access the parent DOM; - panel code cannot read Avi cookies, auth tokens, or local storage; - the content security policy blocks the panel's own network connections (the one exception is downloading a project file through a short-lived link from the bridge); scripts run only from the panel's bundle and its declared chunks, and images and media may load from `https:` URLs; - bridge calls are handled by Avi and checked server-side. This means panel code should treat `useAvi()` as its only Avi integration point. ## Limits and validation Current limits: | Limit | Value | |---|---:| | Panels per app | 5 | | Bundle size per panel | 4 MB | | Lazy chunks per panel | 4, up to 16 MB each | | Panel title length | 60 characters | | Entry extensions | `.tsx`, `.ts`, `.jsx`, `.js` | | Visibility | Every app enabled in the project | Build-time validation rejects: - absolute panel entry paths; - paths that leave the app directory; - unsupported file extensions; - invalid icon values, such as empty strings, very large values, absolute paths, paths outside the app directory, unsupported URL schemes, or non-image data URIs; - Node built-in imports such as `fs`, `path`, or `node:crypto`; - mismatched `react` / `react-dom` versions in the app's dependencies; - bundles over the size limit. ## Troubleshooting **The panel icon does not appear** Confirm the app is enabled for the project, deployed successfully, and has `ui.panels` in `app.ts`. **The build fails with a Node built-in import error** Panel bundles target the browser. Move Node-only work into a tool handler or a shared API called by a tool, then let the panel use bridge APIs. **A bridge call fails with a capability error** Add the required capability to `defineApp({ capabilities })`, redeploy, and approve the capability when enabling the app. **The panel renders but does not share data with tools** Check that both sides use the same declared store. `avi.data.<storeName>` in the panel corresponds to `context.data.<storeName>` in a tool — same store name, same per-store scope. **The iframe shows a load error** Run `avi build` locally first to catch manifest issues, then run `avi deploy` to catch browser-only bundle issues such as Node built-in imports and bundle size. **The panel opens but stays blank** A blank panel usually means the bundled React crashed on startup. Make sure `react` and `react-dom` are both declared in the app's `package.json` at the same version, reinstall, and redeploy. ## Example Start from the starter project: ```bash avi init my-app --dir my-app cd my-app npm install avi deploy ``` Then grow it the way Avi's own apps are built: - a `RecordBrowser` list, with the open record kept in `useAviView`; - a `RecordPage` for the open record, kept live with `useLiveRecord` and `useRecordRefresh`; - the panel and the tools on the same data store (`avi.data.*` in the panel, `context.data.*` in tools). --- <!-- Source: https://avi.run/docs/app-voice --> # Voice in App Panels Avi has a voice mode: users talk to it, and it talks back — while the panels they're looking at respond to speech directly. Panels built with Avi's component library (`@avihq/ui` — see [App React UI](/docs/app-react-ui)) are voice-operable **automatically**: the same components that render your lists, filters, buttons, and dialogs also declare what they answer to when spoken to. There is no separate voice API to integrate — compose the standard components and your panel speaks the same language as Avi's own apps. ## What users can say With your panel open in voice mode, the components you composed answer for it: | Spoken | What happens | |---|---| | "Open the calendar", "show drafts" | Panels answer to their titles and app names; tabs answer to their labels. | | "Open the Stripe one", "open the third invoice", "open the browser base one" | Rows answer by name, by their other names (a sender, a company), and by position. Name fragments are forgiving the way speech is: a compound the recognizer split ("browser base"), a word form ("reply" for "replies"), or one misheard word in a longer fragment still lands — but a spoken number or version never bends, and a fragment two rows share is refused aloud instead of guessed. | | "Only open", "show only open", "filter from June 1 to June 15", "clear the filters" | The standard filter control drives every declared filter group. | | "Sort by date", "newest first", "a to z", "highest first", "sort by name", "closed first" | Sorts apply exactly as a header click would. | | "Type looks good to me", "dictate one more thing", "type in the title Q3 plan", "clear the comment" | Dictation into any library text field (on by default, named by its label) — bare "type …" reaches the focused field; "type in <label> …" names one. The agent types into the same field through the same door, addressing it as `field:<id>`. | | "Send it", "save", "reply" | Action buttons answer to their declared phrases. | | "Open the preview", "show me the preview", "press approve", "click the merge button" | Declared actions also answer through open and press verbs — declare the bare phrase ("preview") and the verbs come free. | | "The first one", "the latest", "the last one", "open number two" | Positions work with or without a verb; "latest" is the top of the list, "last" the bottom. When a record shows its own list (a repo's files), a bare position counts that list. | | "Diff tab", "open the diff tab", "switch to reviews" | Every tab strip answers — the panel's own tabs and a record's nested tabs alike. | | "Go to line 42", "scroll to line 120", "line 7", "jump to line forty two" | In a panel that declares the go-to-line action (an action with id `jump-line`; the Code app's file reader does), the line scrolls into view and lights up. The host adds "go to / scroll to / show me line N" and passes N as the param (`jump-line@42`); bare "line 7" and spelled-out numbers come from your own templates ("line *", "jump to line *"). | | "Rename note to Q3 plan", "call it Budget review", "new folder Plans 2027" | Actions that take free text declare a template phrase ending in `*`; whatever the user says after the prefix reaches your handler verbatim. | | "Expand the second message", "unfold the one from Jane", "collapse the latest email", "fold up the latest" | Blocks that open in place — a message in a thread, a folder in a tree — answer the fold verbs by position, by name, and by sender, and only foldable things answer them (a list row never does). | | "Open the attachment", "open the file" | A bare noun names the only thing of its kind on screen; with several showing, the user hears "say more of the name" instead of a guess. | | "Close" | Closes the innermost thing — a dialog, an open record, a filter menu — exactly like pressing Escape. | | "Close the filter", "hide the filters", "dismiss the dialog" | Close, hide and dismiss all take the name of the open thing — any of the names it declares, singular or plural. | | "Delete the draft" → "yes" | Destructive actions always open a confirmation dialog first; while it's up, only the dialog is listening, so a bare "yes" or "confirm" is safe. | | "Acme engagement letter" (a name dialog is open) | A dialog whose field declares itself the dialog's dictation field takes bare speech as that field's value — it fills the field and never submits ("create" commits). Questions ("what should I call it?") and anything addressed to Avi ("Avi, …") still go to the agent. | | "Scroll down", "scroll the list", "scroll the email list down", "scroll down emails", "scroll the inbox up", "page down in the invoices" | Scrolls the pane the user means — the open record for plain scrolling, the list when named. A list answers to its title, and to its row noun in every shape ("emails", "the email list", "the list of invoices"); the open record answers to its singular noun ("scroll the email down"). | The agent also knows what the panel is showing. With a record open, "what am I looking at?" or "reply to this" work without the user reading anything out — the panel describes its current view (what kind of thing is open, its title, who it's from, how it begins) so the agent can act on "this." Two rules hold everywhere: - **A voice command is exactly a click.** Speaking a target runs the same handler as clicking it — never a parallel code path, never something the pointer couldn't do. - **Only declared things are instantly speakable.** The spoken grammar matches only what the library (or your own registration) declares — it never guesses at your UI. Anything else goes to the agent, which can at best press a visible control by its label, so declare what matters. ## Which components carry voice | Component | What speaks | |---|---| | `AppShell` | The foundation — it hosts the panel's voice registry. **Required**: without an `AppShell` root (or a `VoiceSurfaceProvider` at a custom root), no component in the panel registers anything. (The legacy `PanelShell` does not provide this.) | | `RecordBrowser` | Rows by name, alias, and ordinal (via its `voice` config); the list's sorts via `voiceSorts` — "sort by updated", "newest first" (with controlled `sorting` + `onSortingChange`); "close the record" on the open record; the list itself as a named scroll container ("scroll the invoices down" — its title, narrow title, and row noun); and it describes the current list/record view to the agent. | | `DataTable` | The same row voice for any table (its `voice` prop), plus spoken sorts via `voiceSorts` — "sort by amount", "newest first" (`voice` needs `getRowId` + `onRowClick`; `voiceSorts` needs `sortable`). | | `FilterControl` | The complete spoken filter surface for its declared groups — open, pick options, spoken date ranges, clear all. | | `Tabs` | Every enabled tab with a text label answers to it. | | `Input` / `Textarea` | On by default, named by the field's label (else aria-label, else placeholder) — "the title is Q3 plan" and "set the title to …" replace the value, "type in the comment …" adds to it, "clear the comment" empties it; a focused field takes bare "type …". `voice={{ id, label }}` names it yourself, `voice={false}` opts out; passwords, card numbers and one-time codes never speak. | | `Select` | On by default, named by its label — "set status to completed", "change status to open"; "change status" opens the list, where bare option labels pick and "close" dismisses. Name it with its article (`voice={{ id: "status", label: "the status" }}`) and "set the status to completed" / "change the status" answer too. An option's `voiceAliases` add spoken names beside its label — `["4", "four"]` on an option labelled "Urgent" makes "set priority to 4" work. | | `DatePicker` | On by default, named by its label — "set the due date to next friday", "change due date to the 15th", "clear due date"; a date it cannot read opens the calendar. `voice={{ id: "due", label: "the due date", aliases: ["postpone to"] }}` adds "clear the due date" and extra set-phrases ("postpone to friday"). | | `Checkbox` / `Switch` | On by default, named by the label — "turn on reminders", "uncheck remember me", "toggle notifications", through your `onChange`. Theme words ("dark mode") are Avi's own and never reach your switch. | | `Button` | Opt-in via the `voice` prop — declared phrases only, and only while the button is enabled and mounted. | | `Modal` / `ConfirmDialog` | An open dialog takes exclusive focus for voice too: everything outside it goes quiet, "close" dismisses it, and short confirmations are safe inside it. Its focused (or only) field takes a bare answer ("Acme engagement letter") — it fills, never submits. Declare a dialog's extra phrases with `voiceTargets` on the `Modal`: targets registered by the component that renders it sit outside the dialog and go quiet while it is open. | | `DeleteArmedButton` | The two-step destructive flow — any spoken delete/discard/trash phrasing arms it, and the confirmation itself happens inside the dialog's exclusive scope. | | `Breadcrumbs` | Every clickable crumb answers to its label, "go to <label>" and "back to <label>" — speaking it runs your `onClick`. | ## Declaring spoken rows Lists opt their rows into voice with a small config — a noun, a spoken label, and optionally the other names people use: ```tsx <RecordBrowser title="Invoices" voice={{ noun: "invoice", recordLabel: (invoice) => invoice.vendor, recordPhrases: (invoice) => [invoice.number], }} /* ...records, columns, openId, onOpenChange... */ /> ``` - `noun` is what rows answer to as a type — it resolves "open the third **invoice**" when several lists are visible, and "close the **invoice**" on the open record. - `noun` may also be a function of the record, so one list can carry several nouns — a file tree registers folders as "folder" and files as "file", and "the second folder" counts folders only. `nounAliases` (a list, or a function of the record) adds the other names people use for the noun ("pr" for "pull request" — a multi-word noun's initials are understood automatically). (With a function noun, the open record answers to "close the record" and the list title's singular.) - `recordLabel` is the name a user would say. Return `null` for records with no speakable name — they stay clickable, but voice will never guess at them. - `recordPhrases` adds the other ways people refer to a record — an email's sender, a deal's company. `DataTable` takes the same config as its `voice` prop (`noun`, `nounAliases`, `rowLabel`, `rowPhrases`; it needs `getRowId` and `onRowClick`), so custom list bodies get spoken rows the same way. `recordView` / `rowView` — the view a row click opens — lets the agent open a listed row without a lookup. ## Free-text parameters (template phrases) An action that needs a value the user speaks — a new name, a folder title — declares a **template phrase**: a phrase ending in `*`. The words before the `*` are the literal prefix; everything the user says after it is captured verbatim (casing kept, "and"s and commas kept) and handed to your `invoke` as its parameter. ```tsx useVoiceTargets([ { id: "action:rename-note", label: "rename note", phrases: ["rename note to *", "rename note *", "rename the note to *", "call it *", "change the title to *"], kind: "action", invoke: (value) => { if (value) renameNote(value) }, }, { id: "action:new-folder", label: "new folder", phrases: ["new folder *", "create a folder called *"], kind: "action", invoke: (value) => { if (value) createFolder(value) }, }, ]) ``` Rules: a template is only ever a template (the bare prefix never presses it — give the target a plain `label` or phrase for that); an empty remainder never matches; the longest prefix wins; the prefix must not be a host verb ("open *" or "close *" is refused); and the prefix is matched article-tolerant, so "rename the note to Q3 plan" and "rename note to Q3 plan" are the same ask. A one-word template prefix ("move *", "set *") never captures a host command: "move down", "set the zoom to 120" and "change to dark mode" stay the host's. A multi-word prefix ("zoom to *") is a deliberate claim and keeps its precedence. Templates are matched before plain phrases, so a plain phrase that starts with your template's prefix never reaches its button. ## Setting a value on a listed item "Mark groceries as complete", "change the tax return to priority 3", "move the second task to in progress", "make it urgent": declare ONE action with `updates` — the row noun and the values it can set, each with the ways it's said: ```tsx useVoiceTargets([{ id: "update-task", label: "update a task's status or priority", kind: "action", updates: { noun: "task", // rows whose voice noun is "task" are the subjects values: [ { id: "status:complete", phrases: ["complete", "completed", "done"] }, { id: "priority:4", phrases: ["urgent", "priority 4", "priority four"] }, ], }, invoke: (param) => { const [rowId, valueId] = String(param).split("|"); // rowId "" = "it"/"this" (the open record) }, }]); ``` The host matches only when a verb (mark, set, change, update, make, move, switch, put), a listed row of that noun, and a declared value are ALL said — the row resolved by the same forgiving matcher as "open the tax one" (ordinals, punctuation, compounds, word forms, one misheard word). Anything short of that — "update the notes of this task with the latest…" — goes to the agent untouched. Two rows that both fit is a spoken refusal, never a guess. An action can bring its own verbs with `updates.verbs`, which replace the default set. Any update action also takes the value first, linked to the row by "to", "on" or "from": "add the errands label to groceries", "remove the finance tag from the second task". Tasks declares its label actions this way: ```tsx { id: "add-label", label: "add a label to a task", kind: "action", updates: { noun: "task", verbs: ["add", "put", "apply", "attach"], values: labels.map((l) => ({ id: l.id, phrases: [`${l.name} label`, `${l.name} tag`] })), }, invoke: (param) => { /* "<rowId>|<labelId>" */ }, } ``` Keep the value phrases specific when the verb is common. "Add" plus a bare label name would also catch "add a task to the list", so Tasks makes "add" and "remove" say "label" or "tag", and keeps bare names for its own "tag" and "untag" verbs. An action takes up to 64 values of up to 8 phrases each, and up to 8 verbs of one or two words. Verbs the host owns ("open", "show", "go to" and the like) are dropped. ## Collapsible blocks A block that opens **in place** — a message in a thread, a folder in a tree — is an item like any row, with one extra flag: `collapsible: true`. The fold verbs then reach it ("expand the second message", "collapse the one from Jane", "fold up the latest"), and your `invoke` receives the fold as its parameter — `"collapse"` to close, `"expand"` to open; a plain open ("open the second message") passes nothing and should open it. Keep a spoken open idempotent: an already-open block stays open. ```tsx useVoiceTargets( messages.map((m) => ({ id: `message:${m.id}`, label: `${m.sender} — ${m.date}`, phrases: [m.sender, `the one from ${m.sender}`], noun: "message", nounAliases: ["email"], collapsible: true, kind: "item", invoke: (fold) => (fold === "collapse" ? collapse(m.id) : expand(m.id)), })), ) ``` Only foldable blocks answer the fold verbs — a list row never does — so an alias shared with another list is safe: beside inbox rows (noun "email"), "open the second email" still means the rows (an item's own noun outranks another item's alias), while "expand the second email" counts the messages. Fold verbs are not reserved: a panel that folds its own things ("expand *" over a file tree, "collapse all" over a thread) keeps declaring them, and a declared action still answers by its object ("unfold everything" presses an "expand everything" action). ## Custom targets and screen descriptions For affordances no component carries, two hooks from `@avihq/ui/panel` complete the surface: - `useVoiceTargets([...])` registers your own spoken targets — an id, a label, extra phrases, and an `invoke` that must be the same code path as clicking. Declare as many phrases as an action deserves; the library handles the bookkeeping. - A dialog that waits for one value (a name, a title) marks that field's parameterized action `dictation: true`. While the dialog is open, speech that matches nothing else becomes the action's param — so a user can just say the name instead of a phrase you had to predict. It only takes effect inside an open dialog, only the innermost dialog's field takes it, and the action must fill the field, not submit it. - `useVoiceScreen({...})` describes what the current view *is*, as structured fields — `kind` (`"list"`, `"record"`, or `"draft"`), `noun`, `title`, `from`, a short `snippet` of how the content begins — so the agent can answer "what am I looking at" and act on "this" without a tool call. `RecordBrowser` publishes a baseline for you; add your own only for richer inner views (an open reader, a draft editor) — the innermost view wins. ```tsx useVoiceScreen({ kind: "draft", noun: "email", title: subject, from: recipients.join(", "), snippet: body.slice(0, 200), editing: true, }); ``` ## Making your app more voice-capable - **Cast a generous phrase net.** Declare every natural way to say each action — "send it", "send the email", "send this". An unmatched phrasing is a failed command; extra aliases cost nothing. - **Give lists and rows good nouns.** "Email", "event", "invoice" — nouns are how ordinals resolve and how "close the email" finds its target. - **Add the other names.** People say senders, companies, and participants, not subjects — a row titled "Quarterly invoice" from Stripe should also answer to "Stripe" (`recordPhrases`). - **Keep labels speakable.** A label is what the user must *say* — prefer the natural spoken name ("Q3 report" beats "q3-report.pdf"). Matching tolerates punctuation ("q3 report v2" finds "q3-report_v2.pdf"), but when a label has to stay file-like, also add the speakable stem ("q3-report_v2") as a phrase. - **Describe the screen, with a snippet.** Fill `useVoiceScreen`'s `snippet` and `from` — the agent then knows what the user is reading and can act on "this" immediately. - **Keep affordances the same in every mode.** A voice user doesn't know whether the panel is in read or edit mode — mount the same spoken affordances (especially delete) in both, or a spoken command will behave differently depending on invisible state. - **Confirm destructive actions, always.** Route deletes through `DeleteArmedButton`/`ConfirmDialog` so the destructive step only ever happens inside the dialog's exclusive scope — that discipline is exactly what makes a bare "yes" safe. ### Telling the agent what is selected It is automatic: whatever the person highlights inside a **selection scope** reaches the agent with their next message, typed or spoken, as part of the screen. The panel's voice bridge watches the selection and adds it to the open view's screen state (`selection`, the passage around it as `selectionContext`, or a selected image as `selectionImage`); Avi tells the agent that "this" means exactly that text. (The view must describe itself — `RecordBrowser`'s baseline or your `useVoiceScreen` — or the selection has nothing to attach to.) Selection sharing needs a release of `@avihq/ui` later than 0.3.0. - **Mark where reading happens.** Put `data-selection-scope` on the content area — never the toolbar, list rows or crumbs (a selection there is not what the person is talking about). The library `Editor` already marks its own content, so an app built on it needs nothing. - **Say where, when you know.** A view that can name the place ("lines 41–43", "page 2") reports the selection itself and wins over the automatic one: spread `useSelectionState()` into `useVoiceScreen` with a `selectionWhere`. The Code app's file readers report their line selections this way (`selectionWhere: "lines 41–43"`). - **A renderer that keeps its selection off the DOM** (it draws its own highlight) reports it with `reportSelection(state)` — Drive's Word editor does. --- <!-- Source: https://avi.run/docs/ui-components --> # UI Components Every component in `@avihq/ui` — Avi's UI component library and design tokens — rendered live at https://avi.run/docs/ui-components. The gallery is interactive, so it has no Markdown body; the package is published on npm as [`@avihq/ui`](https://www.npmjs.com/package/@avihq/ui). To build sandboxed React panels with these components, see [App React UI](https://avi.run/docs/app-react-ui.md). --- <!-- Source: https://avi.run/docs/api --> # Public API A public JSON API serves the content of avi.run, so agents and scripts can read Avi's documentation, pricing and trust details without scraping HTML. - Base URL: `https://avi.run/api/v1` - Authentication: none. The API is public and read-only (`GET`, `HEAD`, `OPTIONS`). - CORS: `Access-Control-Allow-Origin: *` - OpenAPI 3.1 description: [https://avi.run/openapi.json](https://avi.run/openapi.json) - RFC 9727 API catalog: [https://avi.run/.well-known/api-catalog](https://avi.run/.well-known/api-catalog) | Method | Path | operationId | Returns | | --- | --- | --- | --- | | GET | `/api/v1` | `getApiIndex` | Describe the Avi site API and list its operations. | | GET | `/api/v1/docs` | `listDocs` | List every Avi documentation page. | | GET | `/api/v1/docs/{slug}` | `getDoc` | Get one Avi documentation page, including its Markdown. | | GET | `/api/v1/pricing` | `getPricing` | Get Avi's plan, prices, credits and pricing FAQ. | | GET | `/api/v1/trust` | `getTrust` | Get Avi's certifications, AI data handling, safeguards and subprocessors. | ```bash curl https://avi.run/api/v1/docs curl https://avi.run/api/v1/docs/getting-started curl https://avi.run/api/v1/pricing curl https://avi.run/api/v1/trust ``` Every operation has a unique operationId, a description, typed parameters and typed response schemas in the OpenAPI document, so it converts directly into function-calling tool definitions. ## Errors Every API error is an RFC 9457 problem document (`Content-Type: application/problem+json`) with a stable `code`, a human-readable `detail`, and a `resolution` that says how to fix the request: ```json { "type": "https://avi.run/docs/api#doc-not-found", "title": "Documentation page not found", "status": 404, "detail": "No Avi documentation page has the slug \"nope\".", "instance": "/api/v1/docs/nope", "code": "doc_not_found", "resolution": "List valid slugs with GET /api/v1/docs, then request GET /api/v1/docs/{slug}.", "documentation_url": "https://avi.run/docs/api#errors" } ``` ### api-route-not-found HTTP 404 — API route not found. List the available operations at /openapi.json or /.well-known/api-catalog, or start from GET /api/v1. ### doc-not-found HTTP 404 — Documentation page not found. List valid slugs with GET /api/v1/docs, then request GET /api/v1/docs/{slug}. ### method-not-allowed HTTP 405 — Method not allowed. The Avi site API is read-only. Use GET (or HEAD) as described in /openapi.json. ## Versioning Stable operations live under `/api/v1`. A breaking change ships under a new major path (`/api/v2`); `/api/v1` keeps working unchanged. ## Markdown for AI agents Every page on avi.run has a Markdown representation. Request any page with `Accept: text/markdown`, or append `.md` to its path (`/index.md` for the home page). Responses carry `Vary: Accept`. Paths that don't exist return HTTP 404 with a Markdown body when Markdown is requested. - [llms.txt](https://avi.run/llms.txt) — an index of the site for language models, with when-to-use guidance. - [llms-full.txt](https://avi.run/llms-full.txt) — the complete documentation in one Markdown file. ## Help Questions about the API: [support@avi.run](mailto:support@avi.run).