Agents & Skills
Studio work is never done by one monolithic model call — it’s done by a small team: one
Agent per role in the plan, each carrying only the skills its tasks need, running through a
bounded tool-calling loop. This section covers the runtime that makes that real; the Skills
catalogue below documents every one of the 25 built-in skills, one page
each, with its exact input and output schema.
The pieces
Agent { id, ownerId, beingId: Id | null, name, role, description, avatar, skills: string[], tier, systemPrompt, isPublic }Team { id, projectId, leadAgentId, memberAgentIds: Id[] }Task { id, projectId, parentId, title, role, assigneeAgentId, skills, tier, dependsOn, status, input, output, error, costUsd }- An
AgentwithbeingId: nullis ephemeral — created for one project’s team, never a citizen. - An
Agentwith a realbeingIdis a full being — one of the twelve seeded citizens, or one you’ve promoted yourself, with its own Soul. GET /api/studio/agents/POST /api/studio/agentslet you save and reuse agents across projects; team assembly reuses a saved agent automatically when its role and skills fit (see Projects).
Personas: how an ephemeral agent gets a voice
Nine built-in personas (packages/agents/src/personas.ts) cover the roles a plan can name — lead,
researcher, writer, editor, engineer, designer, analyst, marketer, and a few more — each with a
name, an avatar, a distinct voice, values, and a default skill set, so a five-agent team reads like
five different people, not one assistant repeating itself in different fonts:
| Role | Name | Voice |
|---|---|---|
| lead | Orin Vale | Calm, decisive, names trade-offs out loud |
| researcher | Ione Marsh | Precise and curious; never states a fact she can’t cite |
| writer | Sol Anders | Warm, vivid, concrete; cuts every needless word |
| editor | Mara Quill | Kind but unsparing; explains every change in one line |
| engineer | Tobiah Reed | Terse and practical; comments on why, not what |
| designer | Lys Arden | Visual and specific: names colours, sizes, spacing |
If the Souls package is available at runtime, an ephemeral agent gets a full, deterministically-generated Soul compiled in “agent” mode instead of the plain persona prompt — same name and working voice, richer personality underneath. Either way, no model call is spent creating an agent; personas and souls are template-generated.
The tool-calling loop
Every task an agent runs goes through runToolLoop() (packages/llm): the agent’s system prompt
(persona/soul + a short “how you work in the Studio” runtime addendum) plus the task instruction,
with its allowed skills offered as OpenAI-style function tools, for up to 10 steps. Every think,
tool.call, tool.result, message and final output becomes a TraceEvent you can watch live —
see Projects for the full mechanics (retries, dependency context, file
sandboxing).
The skill registry
A skill is a plain, typed function: zod input, zod output, a run(input, ctx). SkillContext
carries the calling being, the LLM client, an abort signal, an event emitter, and a handful of
optional adapters (memory, signals, schedule, integrations, connections) that the API wires
in for real — without them, a skill degrades to a clear “not wired” note rather than crashing.
GET /api/studio/skills → SkillDef[] (every registered skill, with its JSON Schema)POST /api/studio/skills/invoke → SkillInvokeResponse { name, input } → { ok, output?, error?, durationMs }curl -s -X POST http://localhost:4000/api/studio/skills/invoke \ -H 'content-type: application/json' -b cookies.txt \ -d '{"name":"math.evaluate","input":{"expression":"sqrt(3^2 + 4^2)"}}'{ "ok": true, "output": { "result": "5", "value": 5, "type": "number" }, "durationMs": 2 }Every agent gets a baseline skill set regardless of role (DEFAULT_AGENT_SKILLS): web.search,
web.fetch, web.research, math.evaluate, files.read, files.write, files.list — so any
agent can research and save its work even if the planner forgot to list those skills explicitly.
World-acting skills need a yes
Seven skills act on the world outside Novaterra, irreversibly and in the being’s name:
email.send, telegram.send,
whatsapp.send, http.request,
http.json, code.execute and
code.scaffold_react_node (WORLD_ACTING_SKILLS in
packages/skills/src/registry.ts). http.json was added to this set after http.request shipped:
it takes the same arbitrary method and body, so gating one without the other would have left a
bypass — an agent refused a POST via one skill could simply switch to the other. A model driving a
tool loop can be talked into calling any of these seven by a web page it fetched or an email it read,
so the refusal lives in code, not in English in a tool description: SkillRegistry.invoke() refuses
every one of them unless the caller passes approved: true, which means a human asked for this
exact action.
Two callers set it: POST /api/studio/skills/invoke (you hit the button yourself, so it’s already
your explicit action) and the desktop’s signal dispatcher, once you answer “Do it” on the decision it
raises. Everything else — an agent’s tool loop in a Studio task, Muse reaching for telegram.send —
is refused, and the refusal puts a kind: 'decision' Signal
on your desktop instead: “Something running for you wants to use <skill>, which acts outside
Novaterra”, with “Do it” / “No” options and the exact attempted input attached as action. The
agent is told plainly that the step is waiting for approval and to carry on with what it can do
without it.
Answering “Do it” actually runs the skill now: dispatchSignalAction()
(apps/api/src/modules/desktop/actions.ts) re-invokes it with approved: true and reports back
whether it succeeded, all inside the same single-use answer that flips the signal from open to
answered — a signal can’t be answered twice, so an approval can’t be replayed.
The skills catalogue
112 skills are named in the canonical BUILTIN_SKILLS list (packages/contracts/src/skills.ts)
as of 2026-09-28 — up from 28 a few days ago, after several waves of promotion added whole families
in a day. The newest are the five editor.* skills gen_editor is made of — derive an editor spec from a sample with no model, propose one with a model only when no shape could be derived, build it and run the editor standard over it, register it only if it passes, and editor.compose, the three deterministic ones in a row. Before them came whatsapp.reply, telegram.reply and sms.reply, which answer a person
who wrote through one of those apps — world-acting, so each asks first. Before them came mail.reply, which answers a letter that came to a person’s
@novaterra.world address from that same address — world-acting, so it asks first, and a model may
draft the reply but never send it. Before it came audio.count, which counts the beats in each part
of a written score without rendering it, so a model writing music checks its sums with a tool
instead of in its head. Before that came a pair, time.shift and time.between: one counts a number of calendar
days forward from a date, the other measures the distance between two dates and says which way round
they are. They exist because five generators that put a legal deadline — or a verdict on one — onto a
document were asking a language model to do that arithmetic, and a sweep of the whole catalogue found
every one of them refused by its own date gate. A deadline, and whether an appeal beat it, are now
counted by a skill and printed by a template; no model is asked for either. They are deliberately two
skills rather than one with a sign, because “three days late” and “three days early” must not share a
field. The four before those are the ones that make video rather than move it: video.card draws a
title card and encodes it, video.caption burns subtitles into the picture, video.voiceover lays
a recorded voice over a film without touching a frame of the picture, and video.fade joins clips
with a cross-fade. They are the first skills here that need a native codec, and the first whose
licence is worth reading before you redistribute a node — see
Installing the app.
This page documents 25 of them in full below, plus the three authoring skills — the rest are listed
by name, grouped, in Everything else further down. Every one
is grounded directly in packages/skills/src/** — input and output schemas are copied from the
real zod definitions, not approximated. BUILTIN_SKILLS itself is the only correct place to read
the current count; a hand-typed number here will go stale the same way “thirty” and “twenty-eight”
already did.
Web — web.search · web.fetch · web.research · http.request
Files — files.list · files.read · files.write · files.search
Code — code.execute · code.scaffold_react_node · scene3d.create
Docs — docs.write_markdown · docs.write_docx
Image — image.generate
Data — math.evaluate
LLM — llm.generate · llm.structured
Memory — memory.recall · memory.store
Desktop — signal.raise · schedule.create
Comms — email.read · email.send · telegram.send · whatsapp.send
Authoring — studio.author_agent · studio.author_generator · studio.author_skill_bundle
These three are the Studio building Novaterra itself, and they are catalogued but have no page of their own yet; the Agent crew & pipeline generator is what drives them, and covers what they do and why they refuse what they refuse.
The first two write a row you own — an agent, or a generator. They are deliberately not
world-acting: nothing leaves the world, no code runs, and a DELETE undoes them. What they are
is owner-scoped by construction — the adapter behind them is built from the calling being’s id, and
no input schema carries an ownerId, so a model has no shape in which to name a different owner.
Every skill name in an authored agent is validated against the live registry before a row is
written, and an unknown name refuses the whole call rather than saving something half-wired.
The third is named for exactly what it is allowed to do. An LLM-authored skill is arbitrary
code, so nothing here authors a skill: the run emits a plugin bundle — a manifest, proposed
source and a REVIEW.md — into the project workspace. It is never registered, never imported,
never executed, and its installed flag is always false. Installing it stays a separate act by a
person: reading it, moving the directory, and POST /api/plugins with the capability grant written
out by hand. See Plugins and extensibility.
docs.write_pdf and http.json are catalogued now
This section used to say these two ran but were missing from BUILTIN_SKILLS, so no generator
could declare them. That gap is closed: both are in the canonical list as of 2026-09-20, and
several generators shipped the same day already declare docs.write_pdf directly — invoice,
estimate, formCompletion, meansStatement, consignmentNote, legalClaim,
appealSubmission, formalComplaint and prescription among them
(apps/api/src/studio/generators/*.ts).
docs.write_pdf is PDF export via pdf-lib, the same markdown pipeline as docs.write_docx.
http.json is a stricter JSON-in/JSON-out http.request that throws on a non-2xx or non-JSON
response instead of returning it, and it is gated as world-acting exactly like http.request — it
takes the same arbitrary method, URL and body, and a skill that can POST anywhere without a human
“yes” would be a way around the gate rather than an exception to it.
Everything else in BUILTIN_SKILLS
The remaining ~60 names, by family, none with an individual page yet — an honest gap, not a hidden
one. Every one is real and callable; read its schema directly in packages/skills/src/** (the
directory under each family’s own name, e.g. packages/skills/src/geo/).
| Family | Skills |
|---|---|
| Records & revision | files.zip · record.append · artefact.revise · artefact.history |
| Repo | repo.tree · repo.log · repo.diff |
| Docs & layout | docs.read_docx · docs.fill_pdf_form · docs.redact_pdf · deck.write · layout.page · layout.pattern |
| Sheets | sheets.read · sheets.query · sheets.write |
| Text | text.split — typed text into rows and typed records, with declared separators and no guessing |
| Time | time.shift — a date plus a number of calendar days, counted exactly · time.between — the distance between two dates, as a direction and a count that is never negative. Neither knows anything about weekends or public holidays, and both say so |
| Image | image.edit · image.info |
| Vector & charts | vector.diagram · vector.drawing · vector.mark · chart.render |
| Planning | plan.assign |
| Invitations | invite.ask · invite.replies |
| Browser | browser.open · browser.read · browser.act |
| Media | media.transcribe · media.speak · media.video · media.ocr |
| Music & score | audio.bed · audio.score · audio.probe · score.engrave |
| Video | video.probe · video.cut · video.join |
| Scene analysis | scene.quote · scene.understand (distinct from scene3d.create above) |
| Studio authoring | studio.author_module |
| ESP32 devices | esp.list · esp.flash · esp.push · esp.read |
| Weather | weather.read |
| Style | style.author_pack · style.derive_system |
| Geo | geo.plot · geo.site_plan · geo.contour |
| Social | social.compose |
| Calendar | calendar.read · calendar.availability · calendar.write · calendar.cancel · calendar.parse_ics · calendar.freebusy |