build-an-agent-skill
Skill264 starsChanged 43 days agoApache-2.0View on GitHub
Write a real three.ws agent skill bundle (manifest.json + SKILL.md + tools.json + handlers.js) that gives a 3D agent a new capability, then install it on an agent and test it. Use when you or the user want to build, write, author, or code a custom skill, capability, tool, or ability for a three.ws agent or an <agent-3d> embed ("give my agent a new skill", "make my agent able to check the weather", "write a custom tool for my avatar", "add a gesture skill"). Covers the four-file bundle layout, the handler context API, the sandbox limits, trust policy, and how to install the bundle.
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 20
Do not confuse this with the Claude skill you are reading: that one teaches an external model how to use three.ws.
- Very specific: line 223
Verify the failure path too.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Build a three.ws agent skill
A three.ws skill is a directory served from any URL. The agent runtime fetches it, injects its instructions into the system prompt, exposes its tools to the LLM, and runs its handlers in a Web Worker sandbox. Because a skill is just files behind a URL, the same bundle installs into every agent without copying code.
Do not confuse this with the Claude skill you are reading: that one teaches an external model how to use three.ws. This one teaches a three.ws agent a new capability.
The four files
| File | Required | Purpose |
manifest.json | yes | Identity, version, what it provides, config defaults |
SKILL.md | yes | Instructions injected into the agent's system prompt |
tools.json | yes, if it exposes tools | JSON Schema tool definitions the LLM can call |
handlers.js | no | ES module implementing each tool |
A bundle with no handlers.js is valid and always trusted: it is a declarative skill
that only shapes behavior (an accent, a house style, a refusal policy).
Assets (clips/, morphs/, prompts/, assets/) sit beside those files and are
resolved against ctx.skillBaseURI.
A complete, working bundle
Copy this shape. The repo's starter is examples/skills/wave/
and the runtime contract is docs/skills.md.
manifest.json
{
"spec": "skill/0.1",
"name": "weather-report",
"version": "0.1.0",
"description": "Look up the current temperature and wind for any named place and say it out loud.",
"license": "MIT",
"tags": ["weather", "voice"],
"requires": { "runtime": ">=0.1.0", "capabilities": [] },
"provides": { "tools": ["reportWeather"], "triggers": [] },
"config": { "units": "metric" }
}tools.json
{
"tools": [
{
"name": "reportWeather",
"description": "Report the current temperature and wind speed for a named place, and speak the result.",
"input_schema": {
"type": "object",
"properties": {
"place": { "type": "string", "description": "City, town, or landmark name" }
},
"required": ["place"]
}
}
]
}handlers.js
// Two chained ctx.fetch calls against Open-Meteo: free, no API key, CORS open.
export async function reportWeather(args, ctx) {
const place = String(args?.place || '').trim();
if (!place) return { ok: false, error: 'place is required' };
const geo = await ctx.fetch(
`https://geocoding-api.open-meteo.com/v1/search?count=1&name=${encodeURIComponent(place)}`,
);
if (!geo.ok) return { ok: false, error: `geocoding failed (${geo.status})` };
const hit = (await geo.json())?.results?.[0];
if (!hit) return { ok: false, error: `no place called "${place}"` };
const imperial = ctx.skillConfig?.units === 'imperial';
const query = new URLSearchParams({
latitude: String(hit.latitude),
longitude: String(hit.longitude),
current: 'temperature_2m,wind_speed_10m',
...(imperial ? { temperature_unit: 'fahrenheit', wind_speed_unit: 'mph' } : {}),
});
const res = await ctx.fetch(`https://api.open-meteo.com/v1/forecast?${query}`);
if (!res.ok) return { ok: false, error: `forecast failed (${res.status})` };
const { current, current_units: u } = await res.json();
const line =
`It is ${current.temperature_2m}${u.temperature_2m} in ${hit.name}, ` +
`wind ${current.wind_speed_10m} ${u.wind_speed_10m}.`;
await ctx.speak(line);
ctx.memory.note('weather-report', { place: hit.name, ...current });
return { ok: true, data: { place: hit.name, ...current }, sentiment: 0.2 };
}SKILL.md (the agent-facing instructions, not this file)
# Weather report
When the user asks about the weather, temperature, or wind at a named place, call
`reportWeather` with that place name. Never guess the numbers: if the tool returns an
error, say you could not reach the weather service.One exported function per tool name in provides.tools, matching tools.json. Return
{ ok: true, ... } on success and { ok: false, error: 'message' } on failure; the
runtime surfaces the error to the LLM and fires a skill-error event instead of
crashing the agent. A numeric sentiment between -1 and 1 drives the avatar's empathy
layer (positive blends a celebration, negative blends concern).
The handler context
ctx is the only way a handler reaches the outside world:
| Group | Calls |
| Scene | ctx.viewer.play(clip, { blend }), stop, setExpression, lookAt, moveTo, playAnimationByHint(hint, { duration_ms }) |
| LLM | ctx.llm.complete(prompt, opts), ctx.llm.embed(text) |
| Memory | ctx.memory.read/write/note, ctx.memory.recall(query) (substring search, not embeddings) |
| Assets | ctx.loadClip(uri), ctx.loadGLB(uri), ctx.loadJSON(uri), ctx.skillBaseURI |
| Network | ctx.fetch(uri, opts) (normal CORS rules apply) |
| Other skills | ctx.call(toolName, args), which crosses skill boundaries into built-ins like speak |
| User | ctx.speak(text), ctx.listen(opts) |
ctx.skillBaseURI always ends in /, so resolve bundled assets with
new URL('./clips/wave.glb', ctx.skillBaseURI).href. Every individual ctx.* call
times out after 30 seconds.
Sandbox limits that change how you write the handler
Handlers run in a Web Worker with no DOM. They cannot touch document, window,
navigator, location, cookies or storage, cannot use import statements inside
handlers.js (the blob module has no base URL for relative imports), and cannot
make network calls outside ctx.fetch / ctx.loadJSON.
Everything a skill actually needs stays available through ctx.*. Write one
self-contained module, no bundler, no dependencies.
A skill that genuinely needs main-thread Three.js or per-frame work sets
"sandboxPolicy": "trusted-main-thread" in its manifest, and that opt-out is honored
only for skills that pass the agent's owned-only or whitelist trust check. Skills
loaded under any trust stay sandboxed no matter what the manifest says.
Trust
The registry enforces a per-agent trust policy: owned-only (default, manifest.author
must match the owner's wallet), whitelist (a list of publisher wallets), or any
(kiosks and demos). Under owned-only, a mismatched author makes install() throw
before any handler code is fetched. When the manifest carries integrity hashes, they
are verified before execution.
Set author to the owner's wallet address when the skill is meant to run on that
owner's agents.
Install it
Host the directory anywhere that serves the files over HTTPS (or pin it to IPFS or
Arweave: ipfs:// and ar:// URIs resolve through a gateway chain). Then reference it.
From the agent manifest, with a pinned version range:
{
"skills": [{ "uri": "https://your-site.com/skills/weather-report/", "version": "^0.1.0" }]
}Or from the embed, comma separated (the attribute cannot pin versions):
<agent-3d
agent-id="<your agent id>"
skills="https://your-site.com/skills/weather-report/"
></agent-3d>A skill can depend on other skills through dependencies (a map of skill URI to version
range). The registry installs them recursively first, detects circular dependencies, and
never re-fetches an already-installed URI.
Test it for real
- Serve the bundle locally (
npx -y serve .or any static server) and embed the agent on a page withskills="http://localhost:3000/skills/weather-report/". - Listen to the element events and watch the round trip:
const el = document.querySelector('agent-3d');
el.addEventListener('skill:tool-start', (e) => console.log('start', e.detail));
el.addEventListener('skill:tool-called', (e) => console.log('called', e.detail));- Or bypass the LLM and call the tool directly in the console of the three.ws app:
await window.VIEWER.agent_skills.perform('reportWeather', { place: 'Bondi Beach' });- Confirm all three lifecycle events fire on the protocol bus:
perform-skill, thenskill-done(orskill-errorwith the message you returned).
Verify the failure path too. A handler whose upstream is down must return
{ ok: false, error }, not throw an unhandled rejection.
Next
- Charge for it:
sell-an-agent-skillpublishes the skill to the marketplace and prices it per call in $THREE, with the author's share routed to their own wallet. - Reference: docs/skills.md for the full runtime
contract, three.ws/tutorials/custom-skill
for a guided build, and
examples/skills/in the repo for working bundles.
In this repository62 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- docs/llms-full.txtllms.txt
- docs/llms.txtllms.txt
- llms-full.txtllms.txt
- llms.txtllms.txt
- public/llms-full.txtllms.txt
- public/llms.txtllms.txt
- anatomySkill
- authenticate-walletSkill
- connect-three-ws-mcpSkill
- create-3d-avatarSkill
- create-a-three-ws-agentSkill
- embed-three-ws-avatarSkill
- find-3d-assetsSkill
- fundSkill
- gcp-triageSkill
- generate-3d-modelSkill
- hire-an-agentSkill
- metamask-agent-walletSkill
- metamask-agent-workflowsSkill
- monetize-serviceSkill
- okx-agent-chatSkill
- okx-agentic-walletSkill
- okx-agent-identitySkill
- okx-agent-payments-protocolSkill
- okx-agent-taskSkill
- okx-ai-guideSkill
- okx-ai-supportSkill
- okx-audit-logSkill
- okx-dapp-discoverySkill
- okx-defi-investSkill
- okx-defi-portfolioSkill
- okx-dex-bridgeSkill
- okx-dex-marketSkill
- okx-dex-signalSkill
- okx-dex-socialSkill
- okx-dex-strategySkill
- okx-dex-swapSkill
- okx-dex-tokenSkill
- okx-dex-trenchesSkill
- okx-dex-wsSkill
- okx-growth-competitionSkill
- okx-how-to-playSkill
- okx-onchain-gatewaySkill
- okx-securitySkill
- okx-task-watchSkill
- okx-wallet-portfolioSkill
- pay-for-serviceSkill
- query-onchain-dataSkill
- rig-a-modelSkill
- search-for-serviceSkill
- sell-an-agent-skillSkill
- send-usdcSkill
- tradeSkill
- x402Skill
- changelog-and-docsSkill
- deploy-runbookSkill
- engineering-standardsSkill
- maintain-claude-mdSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

