--- name: aside description: Integrate aside (aside.pro), the in-app copilot, into a web app — add the embed snippet, register the actions the copilot can run with `aside.register()` (using `aside.fill`, `aside.press`, `aside.highlight`, `aside.waitFor`, `aside.navigate`), mark controls with `data-aside` attributes, and write onboarding flows in markdown. Use this skill whenever the user mentions aside, aside.pro, `window.aside`, `aside.register`, `data-aside`, or asks to add an AI copilot / assistant / in-app agent that fills forms or does things for their users, to "let the assistant create invoices / invite users / change plans", or to build an AI-guided onboarding in their own SaaS — even if they don't name aside explicitly but the project already loads `widget.js` from aside.pro. --- # Integrating aside into a web app > The references this file points to (`references/…`) sit next to it. Read from the web, > they are at `https://aside.pro/skill/references/sdk.md`, `frameworks.md` and `flows.md`. aside is a chat panel that lives inside the customer's web app. The app tells it what it can do — a short list of **actions** like `create_invoice`, `invite_teammate`, `change_plan` — and when a user asks for something, the model picks one action and its arguments. The action's handler is the app's own JavaScript, running in the page: it navigates, fills the form in front of the user, and points at the button the user presses to finish. The model never sees the DOM and never chooses where to click. That split is the whole reliability story, and it is what you are building: **good actions**. Everything else (the panel, the conversation, the knowledge base, the flows engine) is aside's. ## The workflow Work through these in order. Each step says what to look at in the codebase first. ### 1. Learn the app before touching it Find out: the framework (React, Next.js, Vue, Svelte, Angular, plain HTML), the router, how forms are built (native inputs, react-hook-form, Formik, a component library like MUI / Radix / shadcn / Vuetify), and where the logged-in user lives (session, auth context). Then find the 5–15 things users most often need done. Good sources, in order: what the user tells you, support docs or FAQ in the repo, the settings and "create" screens, the first-run / empty-state screens. You need the **workspace id** for the snippet. It is in the aside dashboard (app.aside.pro → the workspace → Widget tab). If the user has not given it, ask — never invent one, and never leave a placeholder they might ship. ### 2. Add the snippet One script tag, loaded once, on every page where the copilot should appear: ```html ``` - `data-workspace` is required (the widget warns and does nothing without it). - `data-user-id` / `data-email`: pass the logged-in user's stable id and email so a conversation and an onboarding's progress follow the person across devices. Without them the widget gives the browser its own anonymous id. - If the workspace has a JWT secret configured, it accepts identity **only** from `data-jwt`: an HS256 token the app's backend signs with that secret (`sub`, `email`, `exp`). Never sign it in the browser — the secret would ship to every visitor. - `data-lang="en"` / `"es"`: only if the app knows its own interface language. Omit it otherwise; aside then follows the workspace's setting. - `data-persona="PERSONA_ID"`: only if the user picked a specific persona in the dashboard. Where it goes depends on the stack — see `references/frameworks.md`. The widget refuses to load twice on one page, so a React effect that runs twice is harmless, but inject it from one place. ### 3. Design the actions This is the step that decides whether the copilot is useful. An action is **a user's intent, not a UI gesture**: | Good | Bad | |---|---| | `create_invoice({customer, amount, due_date})` | `click_new_button()` | | `invite_teammate({email, role})` | `fill_input({selector, value})` | | `change_billing_email({email})` | `go_to_page({url})` as the only action | Why: the model chooses from names and descriptions. `create_invoice` matches "bill John £850 for the boiler"; a generic `fill_input` makes the model guess selectors, which is exactly the unreliable design aside replaced. For each action decide: - **name** — snake_case, starts with a letter, `[a-z0-9_]`, ≤ 64 chars. This is the tool name the model calls. aside silently drops an action with an invalid name, so get it right. - **description** — one or two sentences, ≤ 500 chars, written for the model: what it does, when to use it, and what it does *not* do ("Fills the invite form. The user still presses Send."). Required; an action without one is dropped. - **inputSchema** — a JSON Schema object (`type: 'object'`, `properties`, `required`). Use `enum` for closed choices (roles, plans, currencies) — the model then can't invent a value the form doesn't accept. Describe formats in each property's `description` ("ISO date, YYYY-MM-DD"). - **label** (optional) — short text *for people*, e.g. "invite a teammate". It never reaches the model; the panel's front page offers it as a suggestion chip. Give it to the 3–5 actions a new user most likely wants. Limit: 40 actions per page. Ten sharp ones beat forty vague ones. ### 4. Mark every control with `data-aside` Every element an action touches gets a stable attribute: ```jsx ``` Select only by `[data-aside="..."]` — never by class, text or DOM position. Classes change on every redesign (and are hashed by CSS modules / Tailwind builds), text changes with translations, and a selector that silently matches the wrong element is how a copilot clicks the wrong thing. With `data-aside`, a missing element fails loudly instead (`waitFor ... did not appear`). Use kebab-case, prefixed by screen: `invoice-amount`, `settings-billing-email`. ### 5. Write the handlers ```js const actions = [{ name: 'create_invoice', description: 'Open the new-invoice form and fill in the customer, amount and due date. ' + 'It does not send the invoice: the user reviews it and presses Create.', inputSchema: { type: 'object', properties: { customer: { type: 'string', description: 'Customer name as the user said it' }, amount: { type: 'number', description: 'Total in the account currency' }, due_date: { type: 'string', description: 'ISO date, YYYY-MM-DD' }, }, required: ['customer', 'amount'], }, label: 'create an invoice', run: async ({ customer, amount, due_date }, aside) => { aside.navigate('/invoices/new'); await aside.fill(await aside.waitFor('[data-aside="invoice-customer"]'), customer); await aside.fill('[data-aside="invoice-amount"]', amount); if (due_date) await aside.fill('[data-aside="invoice-due-date"]', due_date); aside.highlight('[data-aside="invoice-submit"]', 8000); return { filled: { customer, amount, due_date }, next: 'the user reviews it and presses Create' }; }, }]; if (window.aside) window.aside.register(actions); else window.addEventListener('aside:ready', () => window.aside.register(actions), { once: true }); ``` The rules that matter, and why: - **An action prepares; the user confirms.** Fill, then `highlight` the submit / send / pay / delete button and return. Never `press` a button that creates, sends, pays, deletes or publishes something. This is the promise that lets a company put an AI in front of its customers: nothing irreversible happens without the user's own click. `press` is for harmless steps — opening a tab, a dialog, a dropdown, a "next" in a wizard. The exception: a settings form that saves itself on change (no submit button) — there the fill *is* the change, so say so in the description. - **Navigate, then wait.** After `aside.navigate(...)` (or opening a dialog) the target isn't rendered yet. `await aside.waitFor(selector)` before filling (default 5 s; pass a longer timeout for slow screens). The whole action must finish within ~15 s or the agent sees a timeout. - **Fail with a sentence the model can repeat.** `throw new Error('There is no customer named "Jon" — ask the user to pick one: ' + names.join(', '))`. The message reaches the model as the tool error; a vague error becomes a vague apology. - **Return what happened, briefly.** The return value is what the model reads next: `{ filled: {...}, next: 'the user presses Save' }`. Don't return DOM nodes or big objects. Returning nothing reports `{ done: true }`. - **The handler is your code.** It may call the app's own store, router or API client instead of driving the DOM (e.g. look up a customer id, prefill state). `aside.fill` exists so the user *sees* the form being filled, which builds trust; prefer it for anything the user will review. - **Validate at the boundary.** The model may pass "blue" to a colour field or "next friday" to a date. Convert what you can, throw a clear error for the rest. - **Resolve names exactly, never by guess.** When an argument names something that exists (a customer, a project, a teammate), match it exactly (case- and accent-insensitive). If nothing or more than one thing matches, throw and list the candidates — don't fall back to "contains" or "starts with". A substring match turns "Jon" into "Mary Jones", and the user ends up with a quote for the wrong customer. - **Start from a clean form.** An action that prepares a new record (lead, quote, invite) sets every field it owns: the ones it was given, and empties the optional ones it wasn't. Otherwise a value from an earlier run or a half-typed draft rides along unseen — a due date from the last quote ends up on this one. - **Prefer the app's state for custom controls.** `aside.fill` works on native ``, `