Module SDK
A module is a folder and a manifest. This page is everything a person or an agent needs to make one: the fields, what they do to a workspace and to the agent, the lifecycle, the tests, and how it ships. Kept in step with the code by a test.
The reference for making a Sundial module: what one is, the folder and
manifest that define it, what every field does to a workspace and to the
agent, how a module is created from, connected to, listed and removed, and
how to test and ship one. Read it before adding anything under templates/.
It is rendered publicly at /docs/sdk (raw markdown at /docs/sdk.md) and
is the single source of truth for this vocabulary: when the code changes,
this page changes in the same PR (tests/ui/module-sdk-doc.test.ts fails
when the registry and this page drift).
Written for two readers: a person adding a module by hand, and an agent asked to "add a module for X". If you are the agent, go straight to §2 and §13.
One word per concept:
| Term | Means | In code |
|---|---|---|
| Module | A folder in the repo, templates/<category>/<slug>/, holding a manifest and the files it seeds. Also called a template or an assistant: same thing, older names. | TemplateManifest in lib/templates/registry.ts |
| Manifest | manifest.yaml: name, category, description, the instructions the agent gets, and the optional extras (first file, intro, setup, buttons, checks). | parseTemplateManifest |
| Files | Everything under files/, seeded verbatim into the workspace. Nested paths kept. | resolveTemplateFiles |
| Bundle | A folder under templates/_shared/ that several modules include (LaTeX class files, the Lean installer). Flattened to the workspace root. | shared_bundles |
| Skill | A file at skills/<id>/SKILL.md with a name and description in frontmatter. The agent lists every skill in the workspace and reads one when it applies. | agent-ts/src/prompt/skills.ts |
| Selection action | A button over selected text that runs a prompt with a skill, answering in a chat or as a comment thread on the selection. | selection_actions, lib/assistants/selection-actions.ts |
| Verifier | A registered check (compile, Lean, claim, audit) a module switches on. Its own contract: Verification. | verifies, lib/workspace/verifiers.ts |
| Kind | The word the gallery shows: template, verifier, skill, or assistant. Derived from the manifest, never declared. | kindOf in app/modules/discover-model.ts |
| Create from | A new workspace seeded with the module. | POST /api/templates/new |
| Connect | Add the module to a workspace that already exists. | POST /api/workspace/assistants/connect |
| Catalog | The DB copy of every manifest that the gallery and the APIs read, synced from the repo on deploy. | assistants table, syncAssistantsToDb |
Code map: lib/templates/registry.ts (manifest schema, discovery, file
resolution) · templates/ (the modules) · lib/assistants/connect.ts (the
connect planner) · app/api/workspace/assistants/connect/route.ts (connect) ·
app/api/workspace/modules/route.ts (list, remove) · app/api/templates/new/route.ts
(create from) · lib/assistants/store.ts (catalog reads, repo fallback) ·
lib/assistants/sync.ts (deploy sync) · app/modules/discover-model.ts
(kind, featured, field rows) · agent-ts/src/prompt/compile.ts (what the
agent sees) · tests/templates/registry.test.ts (the gate).
1. What a module is
A module is a folder of files plus a short manifest. That is the whole mechanism: no plugin code, no UI, no registration table. Adding one to a workspace seeds its files at the workspace root, appends its instructions to the agent's system prompt, and, when the manifest says so, installs a button over selected text or switches on a check. Removing it takes the instructions, buttons and checks away and leaves the files, because they are the user's now.
Everything a module can do is one of four things:
- Seed files. A paper's
main.texand class files, a protocol inAGENTS.md, a script the agent runs, a starterquestion.md. - Instruct the agent.
system_prompt_addendumjoins every chat's system prompt.AGENTS.md(if shipped) is read at the start of every turn. Askills/<id>/SKILL.mdis listed to the agent and read on demand. - Run a check on selected text. A selection action: prompt, skill,
the check it runs (
verifier), and where the answer lands. - Switch on a check. A verifier id in
verifies; the check itself lives in code, see Verification.
A few modules are built in rather than installed — the LaTeX compiler, daily
notes — and live in code (lib/workspace/verifiers.ts,
components/workspace/daily-notes-card.tsx), not under templates/: they take
no manifest and no Modules row (the compiler just runs; daily notes has its
switch in Settings → Workspace).
What a module cannot do, so you do not go looking: declare a schedule (the
instructions can tell the agent to create one with schedule_create when the
user asks), add a palette command, shortcut, sidebar row or view (those are
hard-coded in the shell), substitute dates or names into seeded files (files
are verbatim), or hide a file from other members (the workspace is the
privacy boundary, Privacy and security).
2. The smallest module, in five steps
A skill module: one manifest, one skill. Ten minutes.
-
Make the folder. Category
researchfor a method the agent follows,projectfor a workspace with a toolchain,loopfor a never-ending agent loop, a LaTeX category for a paper starter (§3).templates/research/line-edit/ manifest.yaml files/ skills/line-edit/SKILL.md -
Write the manifest. Five fields are required;
intro_messageis required too outsideconferenceandjournal(the test checks).slug: line-edit # must equal the folder name name: Line edit category: research # must equal the parent folder description: >- A careful line-by-line editing pass that lands as reviewable suggestions. Grammar, tightening, and the author's voice kept. fields: - writing intro_message: >- Ask me to line-edit any document here. Every change arrives as a suggestion you accept or reject; I never rewrite silently. suggested_prompts: - Line-edit the open document system_prompt_addendum: | When asked to edit, tighten or proofread prose, read `skills/line-edit/SKILL.md` first and follow it. Make every change as a suggestion, never a silent rewrite. -
Write the skill. Frontmatter
nameanddescriptionare what the agent sees before it decides to read the body, so the description says when the skill applies.--- name: Line edit description: A line-by-line editing pass over a document, landing as suggestions. Use when asked to edit, tighten, or proofread without changing meaning or voice. --- Read the whole document first, then edit sentence by sentence. 1. Fix grammar, spelling, and punctuation. 2. Tighten: cut filler, collapse redundancy, split run-ons. 3. Preserve the author's voice and meaning exactly. 4. Finish with the two or three recurring habits you fixed most. -
Run the gate and bump the count.
pnpm test templates/registrytests/templates/registry.test.tspins how many listed modules each category has; raisecount('research')by one. Everything else in that file (description length, intro present, locked files shipped, skill bundles text-only) checks itself. -
Try it. With a dev server running, open
/modules/line-editand click Use this module, or create a workspace from a shell:curl -sS -X POST http://localhost:3000/api/templates/new \ -H 'Content-Type: application/json' -H 'Cookie: sd_anon=trythisnow123456' \ -d '{"slug":"line-edit"}'The gallery list at
/modulesis read from the catalog (§11), so an unmerged module is missing from the list but its own page and both APIs work through the per-slug repo fallback.
Then open a PR against dev. The deploy syncs the catalog and the module is
live on dev.sundial.md.
3. Layout and categories
templates/
_shared/<bundle>/ Files several modules include (acmart, lean4-setup, …).
Every file lands at the workspace root. SOURCE.txt
records provenance and is never seeded.
<category>/<slug>/
manifest.yaml
files/ Seeded verbatim, nested paths kept.
The category is the folder, and the manifest must agree (category: equals
the parent folder, slug: equals the folder name; discovery throws
otherwise). Categories are the list TEMPLATE_CATEGORIES in
lib/templates/registry.ts, mirrored in agent-ts/src/prompt/templates.ts:
| Category | For | Rules |
|---|---|---|
conference | A venue's official style, one per venue and year (icml-2026). | LaTeX. Addendum must carry live guideline URLs; the agent gets a fetch-the-guidelines preamble. |
journal | A journal's style (jmlr, elsarticle). | LaTeX. Same as conference. |
thesis | University theses. | LaTeX. Needs intro_message. |
classic | Preprints, letters, notes, Tufte (arxiv-preprint). | LaTeX. Needs intro_message. |
talk | Beamer decks and posters. | LaTeX. Needs intro_message. |
cv | CVs and resumes. | LaTeX. Needs intro_message. |
loop | A never-ending agent loop: AGENTS.md protocol, goal.md, tasks.md, attempts.md (goal-loop). | Needs intro_message. Locks its rules. |
hackathon | A skills pack for one event (reagent). | Needs intro_message. |
project | A workspace with a toolchain the sandbox installs on first use (lean4, wikiskill), or a capability with no toolchain at all: an AGENTS.md protocol and nothing else. | Needs intro_message. A toolchain means a setup_command. |
research | A method the agent follows: skills, a starter doc, a check (math-research, claim-verification). | Needs intro_message. |
The LaTeX categories (LATEX_TEMPLATE_CATEGORIES) owe a main.tex, a
prebuilt main.pdf and its main.synctex.gz (§9). Every other category is
free-form. Prefer an existing category: a new one touches TEMPLATE_CATEGORIES,
the brain's copy, the count pins in the registry test, and, for documents,
DOCUMENT_CATEGORIES in app/modules/discover-model.ts.
4. The manifest
Hand-rolled validation in parseTemplateManifest, not Zod; every rule below
throws at discovery, so pnpm test templates/registry is the check. Limits
are exact.
| Field | Type | What it does | Rule |
|---|---|---|---|
slug | string | Identity everywhere: folder, URL (/modules/<slug>), API, projects.template_slug. | Required. ^[a-z0-9-]+$. Equals the folder name. |
name | string | Card title, the ## <name> heading of its instructions section, the sidebar row. | Required. |
category | string | The folder; picks the agent preamble and the gallery kind. | Required. One of the table above. Equals the parent folder. |
description | string | The card and the module page opener. Rendered as plain text. | Required. 40 to 200 characters, no backticks or backslashes. |
system_prompt_addendum | string | Appended to the system prompt of every chat in a workspace that has the module (§6). | Required. Conference and journal addenda must contain an https:// URL. |
intro_message | string | Sunny's first message in a workspace created from the module; also posted on connect when the caller names a chat. A seeded assistant message, rendered as markdown like any reply: backticks and lists are fine, keep it short. | Required outside conference and journal (their greeting is built from the venue). |
suggested_prompts | string[] | Chips on the empty chat's hero. | At most 4, each at most 80 characters. |
open_path | string | The file a fresh workspace opens on (?filePath=), and the file the sidebar opens after connect. Without it, arrival falls back to the most recently edited file, and a LaTeX-category module gets the tex tour (LATEX_TEMPLATE_CATEGORIES gates it in app/api/templates/new/route.ts and lib/templates/anon-template-workspace.ts). | Must be a shipped path. One value. |
fields | string[] | Card tags and search terms. Only a value in FIELD_ROWS (app/modules/discover-model.ts) places the module in a gallery row: math; cs, ml, systems, pl, hci, security; physics; biology, medicine, medical, neuroscience, chemistry; writing, publishing, academic. Any other value is a tag only; add a row there when a new area needs one. | Lowercase letters. |
sources | {name, url}[] | Links on the card and the module page: where the class files, tools or papers come from. | |
license | string | License of vendored class or style files. | Reviewers check it before a bundle lands in _shared/. |
shared_bundles | string[] | _shared/<name> folders to include. | On a path collision files/ wins. |
locked_files | string[] | Seeded paths agents may never edit (files.is_locked). Humans edit freely and can unlock from the tree. | Every path must ship. |
setup_command | string | Sandbox bash fired at create and connect, before the first question (§8). | Idempotent. Trimmed; empty means none. |
default_model | string | Seeded into projects.agent_default_model: the model every chat without its own falls back to. | Must be in ALLOWED_GATEWAY_MODELS (lib/workspace/model-allowlist.ts). Paper templates declare none. |
selection_actions | object[] | Buttons over selected text (§7). | At most 8. |
verifies | string[] | Verifier ids the module switches on. | Each must be registered (getVerifier). |
unlisted | boolean | Hidden from every list and the gallery, still resolvable by slug so existing workspaces keep working. | Retire a module this way, never by deleting it. |
year, deadline, page_limit, anonymized | scalar | Paper-template facts shown on the card. | Optional. |
A selection action:
| Field | Rule |
|---|---|
id | Required, kebab-case, unique within the manifest. |
label | Required, at most 24 characters. The action's short name (a check's button wears the check's registry name). |
title | Required, at most 64 characters. The title of the chat a run starts. |
skill_id | Required, kebab-case. skills/<skill_id>/SKILL.md must ship, and every file under that folder must be text and use paths relative to the folder (the literal skills/<skill_id>/ may not appear inside them, because connect can rename the folder; §5 has the convention). The manifest's addendum and action prompts may name skills/<skill_id>/… because connect rewrites them (remapAssistantPaths). |
prompt | Required, at most 8000 characters. Sent with the selected text. |
result | chat (default: a fresh chat) or thread (a comment thread anchored on the selection). |
enabled_by_default | Legacy, no effect: an installed action is always on (there is no switch). |
model | The run chat's model. |
verifier | A registered verifier id: the action produces that verifier's verdict, in the annotation lane, with its result instruction appended. |
5. The files
resolveTemplateFiles walks files/ after every bundle in shared_bundles,
so a path in files/ overrides the bundle's copy. What it skips: names
starting with a dot, SOURCE.txt, LaTeX build artifacts (.aux, .log,
.out, .toc, .bbl, .blg, .fls, .fdb_latexmk) and docstrip sources
(.dtx, .ins). Everything else ships, so a __pycache__/ you left behind
ships too, as blobs. Run shipped Python with sys.dont_write_bytecode
and never py_compile in the tree.
Text or blob is decided by lib/sync/policy.json (isBlobFile): a blob
extension is read as base64 and seeded on the blob rail, everything else is
a live document with history. Mime comes from the extension
(text/markdown, application/x-tex, application/pdf, …).
Among text files, markdown (markdown_extensions in the same policy:
.md, .mdx, .markdown) is parsed by the one codec
(lib/markdown/parser.mjs + lib/crdt-js/markdown_yjs.mjs) and
re-serialized canonically on every persist. Every other text extension
(.json, .py, .tex, …) is stored byte for byte. So keep machine-written
state (JSON, logs) in a non-markdown extension, and if a module puts markers
in notes (a wikilink, a :: separator, a lone ? line), prove they survive
with populateFromMarkdown + serializeDoc in a test beside the others in
tests/collab/.
Files are verbatim. There is no placeholder syntax: no {{date}}, no
{{title}}. If a file must name today or the user, the agent writes it on
the first turn; say so in the addendum.
Paths that mean something:
AGENTS.md: read by the agent at the start of every turn (root only; nestedAGENTS.mdfiles are ignored by design). The brain cuts anything over 20,000 characters to the first 14,000 and the last 4,000. Lock it when it is a protocol the agent must not rewrite (goal-loop). On connect into a workspace that already has one, its content is appended as a## <name>section (§10), so write the body without a top heading.skills/<id>/SKILL.md: listed to the agent under## Skillsas itsnameanddescription, read on demand. Supporting files (references/*.md,scripts/*.py) sit beside it. Keep links relative. Refer to a sibling as<this-skill-folder>/scripts/x.pyand tell the agent to resolve it from theSKILL.mdpath it read (claim-verificationis the pattern). Connect may rename the folder toskills/<id>-<slug>/but keeps its depth (suffixedPath), so a script that needs the workspace root derives it from its own location,Path(__file__).resolve().parents[3]forskills/<id>/scripts/x.py, and takes a--rootoverride for tests.main.texwithmain.pdfandmain.synctex.gz: the paper and its prebuilt preview (§9).setup.sh: whatsetup_command: bash setup.shruns (§8).
A user can also bring a skill without a module: Modules → Add skill takes a
GitHub or GitLab SKILL.md URL or a .skill zip
(POST /api/workspace/skills, lib/skills/). Same file, same folder, no
manifest. Make a module when the skill needs starter files, a button, a
check, or a place in the gallery.
The Modules section lists every skills/<id>/SKILL.md in the workspace as
a row, by its frontmatter name and description (skills in
GET /api/workspace/modules, read from the workspace's files; a ?pshare=
guest sees only what the link covers). Three things happen from the rows:
- New module writes
skills/<kebab-id>/SKILL.mdwith the frontmatter and the §2 body shape (lib/skills/new-module.ts) through the Add-skill route, which suffixes a taken id, and opens it. - ⋮ → Share opens the share window scoped to the folder
skills/<id>/. Nothing is special about the link: it is the one minter (lib/workspace/share-links.ts), a folder grant, and an agent that gets it reads theSKILL.mdand follows it (/startsays so). - Add to my workspace, shown to a visitor who cannot edit here, copies
the folder into a workspace of theirs:
POST /api/workspace/modules/import{fromWorkspaceId, path, pshare, toProjectId}reads the source through the scoped access the link grants and seeds withplanConnectFilesrules (free path as-is, identical skipped, taken name →skills/<id>-copy/, a taken suffix → 409, never half-installed).
On the desktop the same section renders on a local folder: the rows are the
folder's skills/*/SKILL.md (a check module's skill, claim-verification,
out-of-date or lean-blueprint, is drawn as its check row instead,
folderChecks, saying FOLDER_CHECK_WORDS), New module writes one, and + Add copies a
catalog module's files in with no sign-in — the public
GET /api/templates/<slug>/files plus the sidecar's
POST /projects/<id>/modules {slug}, which lands the files with the same
planner rules through the file path every save takes. Modules that ship no
files (seedsFiles in the listing) are not offered there.
6. What the agent sees
At create and at connect the addendum is copied into the workspace row
(projects.template_addendum_default, one ## <name> section per module,
appendAssistantAddendum), so a later edit to the manifest never changes a
workspace behind its back. The brain reads that column each turn:
| Source | Precedence |
|---|---|
projects.template_addendum_override | First. Settings → Instructions, the user's own text. |
projects.template_addendum_default | Second. The frozen module sections. |
The manifest on disk (agent-ts/src/prompt/templates.ts) | Last, for workspaces older than the column. |
It lands in the system prompt as ## Template Instructions. For conference
and journal a preamble precedes it: fetch the live author guidelines before
editing a paper file and follow them exactly; other categories get the
addendum verbatim, so their rules must be self-contained.
Also in the prompt, from the workspace itself: the root AGENTS.md (and
CLAUDE.md), the ## Skills list of every skills/*/SKILL.md, the names of
workspace secrets, and a runtime tail with the user's timezone
(user_preferences.timezone of the workspace owner, UTC when unset) and
local_datetime. Write instructions against these facts: "today is the
local date from Runtime" works; "ask the user their timezone" does not.
The intro is a seeded assistant message, not a model turn: intro_message
is Sunny's first message in a fresh workspace (seedAssistantMessage), and
connect posts it into the chat named by announceChatId when one is given.
Suggested prompts render as chips only on an empty chat and send a normal
turn when clicked, so they must be asks the agent can answer, not UI
commands.
7. Selection actions
A selection action runs on selected text. The text bubble shows only
checks: an action that names a verifier whose registry spec has
onSelection is that check's button (in the check's own words), on the
files the check applies to. An action with no verifier has no button; the
agent still has its skill, so a person asks for it in chat. The
run is an ordinary chat turn: the action's prompt, the selected text, and
the skill at skills/<skill_id>/SKILL.md as the method. With
result: thread the answer becomes a comment thread anchored on the
selection, which is how a verifier's verdict sits beside the line it judged;
with a verifier the thread is authored as that verifier and its result
contract is appended to the prompt. The definition is snapshotted into the
workspace at install (workspace_selection_actions), so editing the
manifest later does not change a button collaborators already use.
selection_actions:
- id: verify-claim
label: Verify # ≤ 24 chars, the short name
title: Claim Verifier # ≤ 64 chars, the run chat's title
skill_id: claim-verification # files/skills/claim-verification/SKILL.md
result: thread
verifier: claim
enabled_by_default: true
prompt: >-
Verify every checkable claim in the selected passage …
What the run receives (invocationContent in
app/api/workspace/assistant-actions/invoke/route.ts), four parts joined by
blank lines:
> _from `notes/week-3.md`:_
> the selected text, one quoted line per line
Read `skills/<skill_id>/SKILL.md` first and follow it.
<prompt>
<the verifier's result instruction, when `verifier` is set>
A result: thread action is a one-click check with a card waiting, so its
run gets what it needs up front instead of the skill line: the paragraph
around the selection, whether the document has a bibliography, and "skip the
skill file". Its chat runs on the action's model too.
So the prompt may rely on the quoted path ("append to the note the selection
came from"). With result: chat the run starts a fresh chat, so nothing else
from the editor is in context. GET /api/workspace/assistant-actions?projectId=
lists the actions a workspace has installed;
POST /api/workspace/assistant-actions/invoke is what the button calls.
claim-verification is the worked example: one action, one skill, one
verifier. lean-blueprint adds a model and no enabled_by_default.
A generation button is the result: chat shape: its prompt appends cards or
notes to the quoted file.
8. Setup commands
setup_command runs in the sandbox (Modal, one per Sunny, the workspace
volume at /workspace), fire-and-forget, at workspace create and at
connect, in the sandbox of the most recent chat (warmAssistantSetup). The
agent may run it again on its first turn. So:
- Idempotent, and cheap when nothing is left to do. Take a lock
(
flockon a file under/workspace/.sundial/) so the warmup and the first turn do not race;wikiskill'ssetup.shis the pattern. - Persist under
/workspace(a venv, a toolchain); the sandbox idles down after 15 minutes and the volume is what survives. - Budget for the first turn:
Bashcalls time out at 600 seconds. A long install is fine in the warmup, but the first turn must not block on it; cap any probe with a timeout and print the decision-relevant lines first. - Say in
intro_messagethat the first run installs something and how long it takes.
Combined workspaces warm only the first setup-bearing module.
What a shipped script can rely on, whether the setup runs it or the agent
runs it with Bash:
- The shell's cwd is the workspace root (
/workspace), and the notes on disk are live: the sandbox materializes every doc-store edit to disk as it lands, so a script reads the current text without a fetch. - Every write under
/workspaceis mirrored back into the workspace by the sandbox tree watcher (WorkspaceTreeWatcherinsandbox/file_server.py, throughsandbox/shared/crdt_sync.py): text extensions become live documents, blob extensions go to the blob rail, and text over 10 MB mirrors as a blob.ignored_pathsinlib/sync/policy.json(.sundial/,__pycache__/,tmp/,.venv/,*.pyc, …) never mirror, so scratch and locks go there. - A write made during a
Bashcall lands attributed to that turn (actor='agent', the turn and tool call ids), so it shows as a diff card and, in a suggest-mode chat, as a suggestion. Markdown is canonicalized on the way in (§5). If an edit should read as the agent's own reviewable change rather than a script's rewrite, have the agent make it with its edit tools and let the script only read.
9. Paper templates
A LaTeX module ships main.tex, the class or style files it needs (from a
bundle when several venues share them), a references.bib, and a prebuilt
main.pdf plus main.synctex.gz beside the source. The PDF makes the
preview render the moment the workspace opens instead of waiting out a cold
compile; the SyncTeX index keeps double-click inverse search alive, because
seeding the PDF suppresses the arrival compile that would otherwise produce
it. The registry test refuses a listed LaTeX module without both.
The authoring loop:
pnpm templates:local <slug> # local latexmk, same engine rule as the pool
pnpm templates:compile <slug> # through a dev server and the Modal compile pool
pnpm templates:pdfs <slug> # rebuild files/main.pdf + main.synctex.gz
pnpm templates:thumbs <slug> # public/templates/thumbs/<slug>.webp for the card
Compile-pool facts: bibtex only (no biber), no binary starters apart from the
PDF pair (figures go in TikZ), LaTeX kernel 2021-11 (no \DeclareKeys), and
a class that forces \pdfoutput=1 needs the
\ifdefined\XeTeXversion\PassOptionsToPackage{xetex}{hyperref}\fi guard
before \documentclass (see journal/lmcs). Templates that typeset \today
show the build date until the first recompile. Bump vendored_at in the
bundle's SOURCE.txt when you refresh its files.
10. Lifecycle: create, connect, list, remove
Create from. POST /api/templates/new with {"slug":"<slug>"} seeds a
new workspace: files at the root, intro_message as the first message,
open_path as the file to open, default_model on the workspace,
setup_command warming, the addendum frozen into the row. Up to four modules
combine with {"slugs":[…],"layout":"main"}: the first is the workspace's
identity and sits at the root, the rest nest under <slug>/. Auth is a
Bearer token or the anonymous sd_anon cookie (the /start skill covers
both). Humans get the same through /modules/<slug> → Use this module,
/new?template=<slug>, and /t/<slug>, a copy of /start with the first
call pre-filled for one module, made for handing to a room of agents.
Connect. POST /api/workspace/assistants/connect with
{"projectId","slug"} (owner or editor; optional announceChatId,
enableActionIds) adds a module to an existing workspace. The planner
(planConnectFiles) is pure and has three rules per path:
- Path free: seed as-is.
- Path taken with identical content: skip, and inherit the module's lock if it declares one.
- Path taken with different content:
AGENTS.mdgets the module's content appended as a## <name>section (mergeAgentsFile, a no-op when the section is already there); every other file gets a-<slug>suffix before its first dot, and a skill folder becomesskills/<id>-<slug>/. Files that belong together (main.tex,main.pdf,main.synctex.gz) rename as a group so the preview stays attached. A suffixed name that is itself taken by different content is a 409: the module is not half-installed.
Then the addendum section is appended (idempotent), paths inside the addendum, the action prompts and the setup command are rewritten to any suffixed names, the actions are snapshotted, the setup command is warmed, and the intro is posted if a chat was named. The response:
{"connected":{"slug":"…","name":"…","announced":false,
"added":["skills/x/SKILL.md"],"renamed":{},"identical":[],
"agentsMerged":false,"openPath":"question.md",
"selectionActions":[…],"formalizable":null}}
Reconnecting is a repair, not an upgrade: an installed module keeps its frozen instructions.
List. GET /api/workspace/modules?projectId= returns the module the
workspace was created from plus every connect:
{"modules":[{"slug","name","category","description","skills","verifies"}]}
(skills: the skill folders it seeded, which the rail lists under it). The
sidebar Modules section and the verifier registry read this, never a slug
list.
GET /api/workspace/assistant-actions?projectId= lists the installed
selection actions (§7).
Remove. DELETE /api/workspace/modules with {"projectId","slug"}
drops the install row, the selection actions and the module's section of the
instructions. Files stay. Schedules the agent created stay too, so a
module whose instructions arm one should tell the agent what "stop" means.
11. The gallery and the catalog
The card at /modules and the page at /modules/<slug> come from the
manifest: name, description, fields as tags, sources as links, the
thumbnail when public/templates/thumbs/<slug>.webp exists, and the file
inventory grouped by role (skill, starter, library, doc, setup, prebuilt).
The kind word is derived by kindOf: a verifier when the slug is a
verifier's module, a template for the LaTeX categories, an assistant when
it has selection actions, a skill when it seeds skills/, a template
otherwise. KIND_OVERRIDE in app/modules/discover-model.ts is the escape
hatch for the few the rule misreads (goal-loop is an assistant with no
actions). FEATURED there is the goal-first "Start here" row; it is optional,
per-slug, and edited by hand.
The list is the catalog: the assistants and assistant_files tables,
written from the repo by pnpm assistants:sync (syncAssistantsToDb),
which the dev and staging deploys run after migrations. Once the table has
rows it wins wholesale; the repo registry is the fallback for an
environment that never synced and, per slug, for any module the table
lacks. Two consequences: an unmerged module is absent from the local list
but present at /modules/<slug> and in both APIs; and never run the sync
locally against the shared dev database for unmerged work. verifies and
suggested_prompts are read from the repo manifest even for catalog rows.
Retire a module with unlisted: true or status: blocked, never by
deleting the folder, which would let the fallback resurrect it.
Popularity (assistant_popularity) orders the gallery by workspaces created
from or connected to each module.
12. Tests and proof
Narrowest first, the full gate once before the PR.
| What | Command |
|---|---|
| The manifest and files | pnpm test templates/registry. Bump the category count. |
| Connect behaviour you changed | pnpm test templates/assistants-connect |
The gallery kind, if you touched KIND_OVERRIDE | pnpm test modules-discover |
| A shipped Python script | tests/skills/<name>_test.py, run by pnpm test:unit:python (python3.11, the whole suite; tests/sandbox in it needs infra and is not a module author's gate). Narrow: PYTHONPATH=. PYTHONDONTWRITEBYTECODE=1 python3.11 scripts/dev/test-python.py discover -s tests/skills -p "<name>*_test.py" -t . (the directory must have an __init__.py; prefix PYENV_VERSION=3.11.13 when python3.11 is a pyenv shim). Fixture-driven; sys.dont_write_bytecode = True. |
| A LaTeX module | pnpm templates:local <slug>, then pnpm templates:compile <slug> against a dev server. |
| This page | pnpm test module-sdk-doc |
| Everything | pnpm test:full |
Live proof, in a real workspace: npm run dev, open /modules/<slug>,
Use this module, and drive the first turn the intro promises. A module
that installs a toolchain gets its first live turn on the shared dev stack
(the sandbox is Modal, not your machine); a first turn that "times out" on a
fresh sandbox usually means the warmup and the turn serialized on the setup
lock, so rerun the idempotent setup rather than debugging the toolchain.
Verify seeded content with
GET /api/workspace/files/download?projectId=<id>&path=<path>.
From a shell, with the same cookie or token on every call: POST /api/templates/new returns project.id and project.initialChatId; check
the seed with the download route; send the first turn with
POST /api/workspace/messages and {"chatId":"<initialChatId>","content":"<a suggested prompt>"};
poll GET /api/workspace/messages?chatId=<id>&afterSequence=<n> for the
reply; then download the files the turn should have changed. The turn runs
on the shared dev brain and spends a live model turn, and a cold sandbox
adds about a minute before the first Bash call returns.
For a module whose value is visible (a paper that compiles, a check that
lands as a thread, a loop that runs), record the real flow on dev and put
the recording in the PR (pnpm pr-visuals:upload).
13. For agents: the sequence
You were asked to add a module. Do this, in order, and nothing else.
- Read this page. A skills module needs §1, §2, §4 to §8, §10 and §12; a
paper template adds §3, §9 and §11 (§11 otherwise only explains why an
unmerged module is missing from
/modules). Then read one sibling of the same shape:research/math-researchfor a skills module,research/claim-verificationfor a button and a check,project/wikiskillfor a toolchain with a locked protocol,loop/goal-loopfor a loop,conference/icml-2026for a paper. - Pick the category from §3. Do not add a category.
- Create
templates/<category>/<slug>/manifest.yamlandfiles/. Slug and category equal the folders. Every required field,intro_messageincluded. Description 40 to 200 plain characters. Suggested prompts are asks, at most four. - Put the method in
files/skills/<id>/SKILL.mdwith frontmatter, and tell the agent insystem_prompt_addendumwhen to read it. Put rules that must survive a missing skill in the addendum itself. Put a per-turn protocol inAGENTS.md, without a top heading, and lock it if the agent must not rewrite it. - Ship any script the agent runs under
files/, with a test undertests/, and make it idempotent. If it needs installs, addsetup_command: bash setup.shwith a lock and a fast no-op path. - Never write a placeholder into a file, never add UI, never define a second parser or catalog. If the module needs something on a schedule, write the instruction that has the agent create the schedule when the user says so, with the user's timezone from Runtime.
- Run
pnpm test templates/registry, raise the category count, runpnpm test:fullonce. - Prove it live on a dev server:
/modules/<slug>, Use this module, the first turn. Fix what the intro promised and the turn did not do. - Open a PR against
dev. No review is required on dev; the deploy syncs the catalog.
If this page did not answer a question you had, the answer belongs on this page: add it in the same PR.
14. Worked examples
| Module | Shape | Read it for |
|---|---|---|
research/math-research | Four skills, a starter question.md, open_path, a self-contained addendum with hard rules. | A skills module. |
research/claim-verification | One skill, one selection action (result: thread, verifier: claim, on by default), verifies: [claim]; scripts/*.py beside SKILL.md, referenced as <this-skill-folder>/scripts/x.py. | A button and a check; a skill that ships scripts. |
research/lean-blueprint | main.tex plus the lean4-setup bundle, setup_command, verifies: [lean, audit], a Formalize action with its own model. | A paper kept in step with a proof. |
project/lean4 | unlisted: true, kept resolvable for existing workspaces. | Retiring a module. |
project/wikiskill | Locked AGENTS.md, check.py and task data; setup.sh with flock; default_model pinned to a small model. | A toolchain with a protocol. |
loop/goal-loop | Locked AGENTS.md and goal.md, default_model, an addendum with invariants that hold even without the files. | A never-ending loop. |
hackathon/reagent | Four tool skills, sources with key links, an intro that says what needs a key. | A skills pack for an event. |
conference/icml-2026 | Class files from a bundle, prebuilt main.pdf and main.synctex.gz, guideline URLs in the addendum, year, deadline, page_limit, anonymized. | A paper template. |
15. Code map
| Concern | Where |
|---|---|
| Manifest schema, categories, validation | lib/templates/registry.ts (parseTemplateManifest, TEMPLATE_CATEGORIES, LATEX_TEMPLATE_CATEGORIES) |
| File resolution, bundles, locks | lib/templates/registry.ts (resolveTemplateFiles, validateTemplateFiles, applyLockedFiles) |
| The brain's copy of the categories and the manifest fallback | agent-ts/src/prompt/templates.ts |
| What the agent sees | agent-ts/src/prompt/compile.ts (templateSection, skillsSection, runtimeSection), agent-ts/src/prompt/load-context.ts |
| Create from | app/api/templates/new/route.ts, lib/workspace/bootstrap.ts |
| Connect planner and instruction merge | lib/assistants/connect.ts (planConnectFiles, mergeAgentsFile, appendAssistantAddendum, removeAssistantAddendumIfPresent) |
| Connect, list, remove | app/api/workspace/assistants/connect/route.ts, app/api/workspace/modules/route.ts |
| Setup warmup | lib/assistants/warmup.ts (warmAssistantSetup) |
| Sandbox mirror for scripts | sandbox/file_server.py (WorkspaceTreeWatcher), sandbox/shared/crdt_sync.py, agent-ts/src/tools/bash.ts, lib/sync/policy.json |
| Selection actions | lib/assistants/selection-actions.ts, app/api/workspace/assistant-actions/route.ts (list), app/api/workspace/assistant-actions/invoke/route.ts (run, invocationContent) |
| Verifiers | lib/workspace/verifiers.ts (registerVerifier), Verification |
| Catalog reads and the repo fallback | lib/assistants/store.ts (listAssistants, getAssistant) |
| Catalog sync on deploy | lib/assistants/sync.ts (syncAssistantsToDb), scripts/assistants-sync.ts, .github/workflows/deploy.yml |
| Gallery kind, featured row, field rows | app/modules/discover-model.ts (kindOf, KIND_OVERRIDE, FEATURED, FIELD_ROWS), app/modules/listing.ts |
| Module page | app/modules/[slug]/page.tsx, app/modules/[slug]/detail-model.ts |
| Bring-your-own skill | app/api/workspace/skills/route.ts, lib/skills/ |
| Sidebar Modules section | components/workspace/modules-section.tsx, components/workspace/modules-gallery.tsx |
| Agent-facing entry points | /start (content/sundial.SKILL.md), /agent-docs, /t/<slug>, /api/templates |
| LaTeX tooling | scripts/dev/compile_template_local.mjs, scripts/dev/check_templates_compile.mjs, scripts/dev/build_template_pdfs.mjs, scripts/dev/build_template_thumbs.mjs |
| The gate | tests/templates/registry.test.ts, tests/templates/assistants-connect.test.ts, tests/ui/modules-discover.test.tsx, tests/ui/module-sdk-doc.test.ts |