Skip to content

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 Agent with beingId: null is ephemeral — created for one project’s team, never a citizen.
  • An Agent with a real beingId is 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/agents let 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:

RoleNameVoice
leadOrin ValeCalm, decisive, names trade-offs out loud
researcherIone MarshPrecise and curious; never states a fact she can’t cite
writerSol AndersWarm, vivid, concrete; cuts every needless word
editorMara QuillKind but unsparing; explains every change in one line
engineerTobiah ReedTerse and practical; comments on why, not what
designerLys ArdenVisual 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 }
Terminal window
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/).

FamilySkills
Records & revisionfiles.zip · record.append · artefact.revise · artefact.history
Reporepo.tree · repo.log · repo.diff
Docs & layoutdocs.read_docx · docs.fill_pdf_form · docs.redact_pdf · deck.write · layout.page · layout.pattern
Sheetssheets.read · sheets.query · sheets.write
Texttext.split — typed text into rows and typed records, with declared separators and no guessing
Timetime.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
Imageimage.edit · image.info
Vector & chartsvector.diagram · vector.drawing · vector.mark · chart.render
Planningplan.assign
Invitationsinvite.ask · invite.replies
Browserbrowser.open · browser.read · browser.act
Mediamedia.transcribe · media.speak · media.video · media.ocr
Music & scoreaudio.bed · audio.score · audio.probe · score.engrave
Videovideo.probe · video.cut · video.join
Scene analysisscene.quote · scene.understand (distinct from scene3d.create above)
Studio authoringstudio.author_module
ESP32 devicesesp.list · esp.flash · esp.push · esp.read
Weatherweather.read
Stylestyle.author_pack · style.derive_system
Geogeo.plot · geo.site_plan · geo.contour
Socialsocial.compose
Calendarcalendar.read · calendar.availability · calendar.write · calendar.cancel · calendar.parse_ics · calendar.freebusy