Your tool's files
A hosted tool is a folder of plain browser files. The AI Builder writes the folder for you; Download Code hands it to you as a zip, and Upload Code takes it back. Nothing in it is compiled, bundled, or installed — what you upload is what runs.
The folder
A typical tool:
my-tool/
├── manifest.json how to load the tool, and the locations it offers
├── app.js the entry: defines the custom element
├── styles.css the tool's styles
├── index.html the tool's markup
└── lib/
└── scoring.js another module, imported by app.js
| File | Role |
|---|---|
The entry (app.js) | Required. An ES module that defines your element and calls customElements.define. It is the only file Voshi imports; everything else it loads is what the entry imports or the manifest names. |
Stylesheets (.css) | Voshi loads them and adopts them into your element's shadow root before it renders, so your styles apply without you doing anything. See Assets. |
Templates (.html) | Markup fragments — no <html>, <head>, <body>, <script>, or <style>. Voshi loads them and you render one with api.assets.template('index.html'). |
Other modules (.js, .mjs) | Imported by the entry or by each other, with relative paths: import { score } from './lib/scoring.js'. The .js extension is required; the browser has no module resolver. A .json file can be imported too: import words from './words.json' with { type: 'json' }. |
| Everything else | Images, fonts, data files. Reach one with api.assets.url('map.png'). |
Everything in the folder is uploaded, at its own path. Left out on purpose: dotfiles and dot-folders (.git, .env), node_modules, __MACOSX, and AGENTS.md. Anything else you leave in the folder — a README, a scratch preview.html — is published with the tool, so delete what you don't want served.
Limits
| Limit | |
|---|---|
| One file | 10 MB |
| The whole tool | 50 MB, 200 files |
manifest.json | 64 KB, at most 200 locations |
| Stylesheets and templates together | 20, because every one is fetched on every launch |
| Paths | Letters, digits, ., _, -; no name starting with -; at most 4 folders deep. Two paths that differ only in case are refused. |
manifest.json
The manifest sits at the root of the folder and says two things: how Voshi should load the tool, and which locations it offers. It does not list the tool's files.
{
"manifest_version": 2,
"app": {
"entry": "app.js",
"tag": "flashcard-drill",
"styles": ["styles.css"],
"templates": ["index.html"]
},
"locations": {
"home": {
"type": "assessment",
"label": "Flashcard Drill",
"description": "Ten accounting terms, flipped and self-marked.",
"points": 10
}
}
}
| Key | Meaning |
|---|---|
manifest_version | 2. (A version 1 manifest has no app section and is still accepted.) |
app.entry | The entry module. Without it, Voshi uses the folder's only .js file, or else app.js. |
app.tag | The custom element tag the entry defines. Optional, but if present it must match. |
app.styles | The stylesheets to load, in order. Leave it out to load every .css in the folder; [] loads none. |
app.templates | The templates to load. Leave it out to load every .html in the folder; [] loads none. |
locations, grouped_locations | The locations the tool offers, in the same shape a self-hosted tool serves. See Locations for what uploading one does to the Locations tab. |
A manifest can declare the locations and leave app out, or the other way round. An upload with no manifest.json at all keeps the tool's locations exactly as they are.
Editing the code yourself
The AI Builder is one way to write a tool, not the only one. The code it produces is ordinary and readable, and the loop for editing it elsewhere is:
From the Running Tool panel. The zip holds the draft, plus an AGENTS.md written for a coding assistant: it states the rules of a hosted tool, lists the tool's locations, and says what the platform will refuse. Point Claude Code, Cursor, or whatever you use at the folder and it knows what it is editing. The file is skipped on upload, so leave it there.
Any editor. There is no build step, so there is nothing to install or run. For layout work, AGENTS.md includes a small preview.html you can serve from the folder with any static server; grades and storage only work in a real launch, so test those in the Sandbox.
Choose the folder. The upload replaces the draft, and the Running Tool panel reloads. If the upload is refused, the message names the file and the rule — a missing import, a bare module name, an entry that defines a different tag than the manifest says.
Every upload replaces the whole draft. The previous draft is gone; there is no history of drafts. Keep your own copy of code you care about, and publish when a draft is worth keeping — a published version is permanent.
The api.assets rule
Your files are served from a directory named by their content hash, so every upload and every version lives at a different address. Never write an absolute URL to one of your own files, and never fetch styles.css or index.html yourself: Voshi has already loaded them. Use api.assets — template(), styles, url() — and the right copy is always the one you get.
What the platform refuses
These are the upload's own messages, so you can recognize them:
manifest.json names styles.css, which is not in the uploadMore than one .js file could be the entry (a.js, b.js): add a manifest.json whose app.entry names it, or name it app.jsThe upload has no .js file: upload the module that defines the custom elementapp.js imports ./lib/cards.js, which is not in the uploadapp.js imports ./styles.css: the page loads stylesheets itself (every .css, or manifest.json app.styles)app.js imports 'lit', a bare module name, and nothing on the page can resolve oneapp.js imports ../../x.js, which is outside the toolmanifest.json says app.tag is 'my-tool', and app.js defines 'other-tool': make them the sameApp.js and app.js differ only in case; rename one of themmedia/video.mp4 is larger than 10 MB/The tool is larger than 50 MB in total