Skip to main content
Version: 2026-10-13 (current)

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 Replace 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
├── 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
FileRole
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. A relative url() inside one works; @import does not, so list each stylesheet in the manifest. See Assets.
Templates (.html)Markup fragments — no <html>, <head>, <body>, <script>, or <style>. You render one with await api.assets.getTemplate('index.html'). One file per view; a view's repeated pieces are <template id> blocks inside it, reached as await api.assets.getTemplate('index.html', 'row'). See Organizing templates.
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 elseImages, fonts, data files. Reach one with api.assets.url('map.png'), or read a data file with await api.assets.getJSON('words.json') / await api.assets.getFile('notes.txt').

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.

One name is reserved: Voshi writes a voshi.json beside your files when it publishes them, so a voshi.json at the root of your folder is refused. One in a subfolder is an ordinary file.

Limits​

Limit
One file10 MB
The whole tool50 MB, 200 files
The upload25 MB once zipped. The dashboard zips the folder for you, so this only matters for large media that does not compress.
manifest.json64 KB
Stylesheets and templates together20, because every one is fetched on every launch
PathsLetters, 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 one thing: how Voshi should load the tool. It does not list the tool's files, and it does not declare the tool's locations, which live on the dashboard's Locations tab.

{
"manifest_version": 2,
"app": {
"entry": "app.js",
"tag": "flashcard-drill",
"styles": ["styles.css"],
"templates": ["index.html"]
}
}
KeyMeaning
manifest_version2. (A version 1 manifest has no app section and is still accepted.)
app.entryThe entry module. Without it, Voshi uses the folder's only .js file, or else app.js.
app.tagThe custom element tag the entry defines. Optional, but if present it must match.
app.stylesThe stylesheets to load, in order. Leave it out to load every .css in the folder; [] loads none.
app.templatesThe templates to load. Leave it out to load every .html (or .htm) in the folder; [] loads none.

A folder with no manifest.json at all is fine when there is one .js file, or one called app.js. A manifest with a locations or grouped_locations key — the way tools declared them before October 2026 — is refused; remove the key, and manage the locations on the Locations tab instead. Keys Voshi does not know at the top level are ignored.

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:

Download Code

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.

Edit

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.

Replace Code

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.

warning

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, never save one in storage, and never fetch styles.css or index.html yourself: Voshi has already loaded them. Use api.assets — getTemplate(), getJSON(), url() — and the right copy is always the one you get. A relative import, new URL(name, import.meta.url), and a relative url() in a stylesheet all resolve to the running copy too.

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 upload
  • More 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.js
  • The upload has no .js file: upload the module that defines the custom element
  • app.js imports ./lib/cards.js, which is not in the upload
  • app.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 one: import your own files with a relative path (./lib.js), and do not use packages or CDNs
  • app.js imports ../../x.js, which is outside the tool
  • manifest.json says app.tag is 'my-tool', and app.js defines 'other-tool': make them the same
  • manifest.json: locations are no longer read from the manifest. A tool's locations are managed on the dashboard's Locations page …
  • voshi.json is the file the platform writes beside yours: rename it
  • App.js and app.js differ only in case; rename one of them
  • media/video.mp4 is larger than 10 MB / The tool is larger than 50 MB in total