Build your first plugin
This guide builds a small marketplace for a spectroscopy lab. Its plugin finds peaks in spectrum files, has a reviewer check them, and writes a report. By the end you will have used every common piece of a plugin.
You need a CLIO installation you can run clio-agent on, and a folder to work in. Plugin code runs inside CLIO, which provides clio_agent.sdk, so there is nothing else to install.
Here is what you will build:
Directoryspectro-lab/
Directoryai.iowarp.clio/
- marketplace.json
Directoryagents/
- spectro.py
Directoryplugins/
Directoryspectroscopy/
- plugin.json
Directoryskills/
Directorypeak-fitting/
- SKILL.md
Directoryai.iowarp.clio/
- plugin.py
Directorytools/
- peaks.py
Directoryprompt/
- capabilities.md
- peak_reviewer.md
Directoryhooks/
- raw_data.py
Directorysubagents/
- reviewer.py
Directoryflows/
- find_all.py
1. Name the marketplace
Section titled “1. Name the marketplace”A marketplace needs one file that names it:
{"name": "spectro-lab", "description": "Our lab's spectroscopy plugin and agent."}Agents in this marketplace are addressed as spectro-lab/<agent id>.
A plugin kept in another repository can be listed too, pinned by commit; installing the marketplace fetches it at that commit:
{ "name": "spectro-lab", "description": "Our lab's spectroscopy plugin and agent.", "plugins": [ { "name": "wtfp", "source": {"source": "git-subdir", "url": "https://github.com/JaimeCernuda/wtf-p", "path": "clio-plugin", "sha": "e23d3cf8f1967053ecedade0522b1a7dff6fe180"} } ]}A source without a commit is reported and not followed.
2. Describe the plugin
Section titled “2. Describe the plugin”plugin.json is the plugin’s manifest. name is its id: lowercase letters, digits, - and .. CLIO’s own settings go under extensions["ai.iowarp.clio"]. Here the plugin requires workspace, the plugin that reads and writes files, because its reviewer reads the raw spectra.
{ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "spectroscopy", "version": "0.1.0", "description": "Find peaks in spectra, review them, and report.", "author": {"name": "Spectroscopy Lab"}, "license": "BSD-3-Clause", "extensions": {"ai.iowarp.clio": {"requires": ["workspace"]}}}plugin.py declares the same identity in Python. It is optional; when present, its id, version and requirements must match plugin.json.
from clio_agent.sdk import Plugin
class Spectroscopy(Plugin): id = "spectroscopy" version = "0.1.0" requires = ["workspace"]3. Add a tool
Section titled “3. Add a tool”A tool is a Python function marked with @tool. Its name, its typed arguments and its docstring are what the model sees, so write the docstring for someone deciding whether to call it. read_only=True tells CLIO it changes nothing.
"""Peak finding for two-column spectrum files."""
import csvfrom pathlib import Path
from clio_agent.sdk import tool
@tool(read_only=True)def find_peaks(path: str, min_height: float = 0.0) -> dict: """Find the local maxima in a two-column CSV spectrum (x, intensity).
Returns each peak's position and height, highest first. """ rows = [(float(x), float(y)) for x, y in csv.reader(Path(path).open())] peaks = [ {"x": rows[i][0], "height": rows[i][1]} for i in range(1, len(rows) - 1) if rows[i - 1][1] < rows[i][1] >= rows[i + 1][1] and rows[i][1] >= min_height ] return {"file": path, "peaks": sorted(peaks, key=lambda p: -p["height"])}Tool names are shared by every plugin in a conversation. Pick a specific name: two plugins that both bring find_peaks cannot be used together.
4. Tell the agent what it cannot infer
Section titled “4. Tell the agent what it cannot infer”A prompt fragment is a short Markdown file with frontmatter. It reaches the prompt only when its when condition holds, here only when find_peaks is available. Keep it to what the model cannot learn from the tool’s own description; CLIO warns above about 150 tokens.
---id: spectroscopy.peakssection: capabilitieswhen: {tools: [find_peaks]}---Spectrum files are two-column CSV (x, intensity). The raw files in data/raw/ are read-only.The id must start with the plugin’s id. A plugin writes to the capabilities or domain section.
5. Add a skill
Section titled “5. Add a skill”A skill is knowledge the agent loads only when it needs it. Only its name and description are in the prompt. The folder name must equal name.
---name: peak-fittingdescription: How to choose a peak-height threshold and judge whether a peak is noise.---
# Peak fitting
Estimate the noise level from a flat region of the spectrum first...6. Protect the raw data with a hook
Section titled “6. Protect the raw data with a hook”A hook runs on an event. This one runs before the file-writing tools and refuses edits under data/raw/. It returns None to allow the call, or a decision of deny with a reason the agent sees.
from clio_agent.sdk import hook
@hook("tool_call", tools=r"fs_write_file|fs_edit_file")def protect_raw_data(ev): """Refuse edits to the instrument's original files in data/raw/.""" path = "/" + str((ev.tool_input or {}).get("filepath", "")).replace("\\", "/") if "/data/raw/" in path: return {"decision": "deny", "reason": "data/raw/ holds the instrument's original files."} return Nonetools is a regular expression that must match the whole tool name. The hook’s id is spectroscopy.protect_raw_data.
7. Add a reviewer subagent
Section titled “7. Add a reviewer subagent”A subagent is a specialist the agent hands work to. It runs as its own conversation, with its own prompt and only the plugins and tools you give it, and returns an answer.
from clio_agent.sdk import PromptFile, Subagent
peak_reviewer = Subagent( "peak-reviewer", prompt=PromptFile("prompt/peak_reviewer.md"), description="Checks found peaks against the raw spectra and says which ones are noise.", plugins=["workspace"], tools=["fs_read_file", "find_peaks"],)description tells the delegating agent when to use it. tools lists the only tools it may call; each must come from its plugins or from this plugin. The prompt file is plain Markdown, relative to ai.iowarp.clio/:
You review peaks found in spectra. For each file, say which peaks are real andwhich look like noise, and why. Read the raw file when a peak is in doubt.A Markdown file under prompt/ without a section in its frontmatter is a prompt file, not a fragment.
8. Write the procedure as a flow
Section titled “8. Write the procedure as a flow”A flow is a procedure in Python. This one finds the peaks in every file, at most eight at a time, sends all the results to the reviewer, asks the person, and writes the report.
from clio_agent.sdk import Flow
class FindAll(Flow): """Find peaks in every spectrum, have the reviewer check them, then write a report."""
id = "find-all-peaks" description = "Find peaks in a list of spectrum files, review them, write a report." min_height: float = 0.0
async def forward(self, files, ctx): if isinstance(files, str): # started from a message: one path per line files = [line.strip() for line in files.splitlines() if line.strip()] found = await ctx.gather( [ctx.call("find_peaks", {"path": f, "min_height": self.min_height}) for f in files], limit=8, ) review = await ctx.call("peak-reviewer", {"results": [r.value for r in found]}) if not await ctx.ask("Peaks reviewed. Write the report?", schema=bool): return review await ctx.step(ctx.workspace.write, "reports/peaks.md", review.answer) return {"answer": review.answer, "report": "reports/peaks.md"}What each line does:
ctx.call("find_peaks", {...})calls the tool by name with its arguments.ctx.gatherruns the calls together, eight at a time, and returns their results in order.- A tool’s result is in
.value. The flow passes plain data to the reviewer, which receives it as JSON. ctx.call("peak-reviewer", ...)starts the subagent as a child task and waits for its answer,review.answer.ctx.ask(..., schema=bool)posts a yes/no question in the conversation and waits for the person.ctx.step(...)runs the write once and records it, so a restart does not write the file again.min_heightis an option. Its type annotation means CLIO checks any value given for it.
If CLIO restarts while this runs, the flow runs again from the top: finished calls return their recorded results, running ones are picked up where they are, and nothing is done twice. Write a flow explains the rules this needs.
9. Add an agent
Section titled “9. Add an agent”An agent is an id, a name, a role and its plugins. This one starts from the base Agent, which carries core and memory, and adds file access, subagents and the tools that start flows.
from clio_agent.sdk import Agent, Requires
class SpectroLab(Agent): """A spectroscopy assistant."""
id = "spectro-lab" version = "0.1.0" name = "Spectro Lab" role = "a spectroscopy analysis assistant" plugins = Agent.plugins + ["workspace", "delegation", "orchestration", "spectroscopy"] requires = Requires(tool_calling=True)workspace, delegation and orchestration come from other marketplaces; a bare id finds them. You do not need an agent of your own to use the plugin: you can add spectroscopy to any conversation with Add plugin in the Agent panel instead. See Agents and plugins.
10. Check it
Section titled “10. Check it”clio-agent plugin check loads the folder exactly as an install would and resolves every plugin and agent in it:
$ clio-agent plugin check spectro-labplugin check spectro-labok plugin spectroscopy@0.1.0ok agent spectro-lab/spectro-lab2/2 passedA mistake is reported with the file and a code, and the command exits with status 1. For example, a misspelled prompt file:
FAIL plugin spectroscopy@0.1.0 error: subagent 'peak-reviewer': prompt file ai.iowarp.clio/prompt/peak_reviewr.md does not exist [prompt_file_missing]0/2 passed -- check failedThe output also lists warnings, for example fragments above the size budget. Warnings do not fail the check.
11. Install and trust it
Section titled “11. Install and trust it”-
Install the folder as a marketplace. In the app, open Settings, Plugins & marketplaces, choose Install marketplace, and enter the folder’s path. Or run, on the machine that runs CLIO:
Terminal window $ clio-agent plugin install spectro-labinstalled spectro-lab (agent-plugins): 1 plugins, 0 agents -- run the trust step: clio-agent plugin trust spectro-labUntil you trust it, CLIO reads only the marketplace’s text. Its agent classes are not loaded yet, which is why it reports 0 agents.
-
Read the code, then run the trust step:
Terminal window $ clio-agent plugin trust spectro-labtrusted spectro-lab: 1 plugins, 1 agents -
Confirm the agent resolves. Each plugin is listed with where it came from:
Terminal window $ clio-agent plugin resolve spectro-lab/spectro-labcore@1.0.0 [core]memory@1.0.0 [agent]workspace@1.1.0 [spectro-lab]delegation@1.0.0 [spectro-lab]orchestration@1.0.0 [spectro-lab]spectroscopy@0.1.0 [spectro-lab]fleet@1.0.0 [required by core]removed:Add
--promptto print the composed prompt. It starts with “You are Spectro Lab, a spectroscopy analysis assistant.” and includes the fragment from step 4.
Add and manage marketplaces covers the same steps in the app.
12. Use it
Section titled “12. Use it”Start a conversation, choose Spectro Lab as its agent (see Pick an agent for a conversation), and ask in your own words:
Find the peaks in every CSV file in data/raw/ and have them reviewed.The agent can call find_peaks itself, hand the files to peak-reviewer, or start the find-all-peaks flow with its run_flow tool. A flow it starts runs as background work: it appears as a card you can follow, and its question appears in the conversation. See Flows and workflows.
13. Change it
Section titled “13. Change it”CLIO reads the installed copy, so after you edit a file:
-
Run
clio-agent plugin check spectro-labagain. -
Run
clio-agent plugin update spectro-lab, or choose Update in the app. -
If you changed code (any
.pyfile,mcp.jsonor a hook), the update is held until you run the trust step again, and the version you trusted earlier stays in use:Terminal window $ clio-agent plugin update spectro-labinstalled spectro-lab (agent-plugins): 0 plugins, 1 agentsheld until trusted (executable parts changed): spectroscopy$ clio-agent plugin trust spectro-labtrusted spectro-lab: 1 plugins, 1 agentsA change to text only, such as a skill or a fragment, needs no trust step.
The next conversation uses the new version. A conversation already running keeps the version it started with.