Building & deploying a skill
Build a skill from scratch and deploy it with one command. The two kinds of skill, how a skill declares its contract, custom tools, subagents — and, when you need it, bundling several skills as a pack.
This guide builds a skill from scratch and ships it — the happy path is
deploy a single skill: scaffold one directory, run puras deploy, done.
By the end you'll know the two kinds of skill, how a skill declares its
contract, and how a skill calls tools and subagents. The worked example
also shows how to bundle two skills together (a pack), covered in the
advanced section near the end —
but you never need a pack to deploy. Read skill-yaml-reference for the
field-by-field manifest spec; this guide is the shape and the why.
The example is hello-world: give it a name and it builds a little greeting card. No external APIs and no media, so it's cheap to run and easy to read. It happens to ship two skills so it can show off subagents — but read it as "a skill, plus a second skill it leans on," not as a mandatory bundle.
Scaffold it straight into a fresh directory with
puras init --template hello-world, or clone the finished version at
github.com/PurasAI/hello-world.
Starting your own from a blank slate? Default puras init gives you a single
starter skill (the layout mirrored at
github.com/PurasAI/skillpack-template).
What a skill is
A skill is a directory containing a skill.yaml — the unit you build
and deploy. The folder name is the skill name; the skill.yaml is its
contract. To deploy one skill, you point puras deploy at its directory:
the first deploy auto-creates the remote target, and from then on it's
edit → puras deploy → puras run.
The entrypoint suffix decides the kind: a Python file.py:func entrypoint
is deterministic; a markdown SKILL.md entrypoint (plus a text_model) is
agentic. Everything else is the same skill.yaml contract.
hello-world is built from two skills, one of each kind — the second exists so the first can call it as a subagent:
| Skill | Kind | Entrypoint | What it shows |
|---|---|---|---|
greeter | agentic | SKILL.md | a custom tool + two subagents, driven by a model |
formatter | deterministic | scripts/format.py:run | a plain Python skill — no LLM |
hello-world/ # the directory you deploy
├── greeter/ # each top-level dir with a skill.yaml is a skill
│ ├── skill.yaml # manifest: schemas, model, the `emphasize` tool
│ ├── SKILL.md # the agent's system prompt (the entrypoint)
│ ├── tools/emphasize.py # custom tool — run(text) -> {loud}
│ └── references/poet.md # a subagent's prompt, read on demand
└── formatter/
├── skill.yaml # manifest: schemas (deterministic skill)
└── scripts/format.py # run(name, shout, poem) -> {card}
A single-skill project is just the same layout with one top-level skill
directory; there's no skills/ wrapper and no root manifest, so going from one
skill to several is purely "add another top-level folder."
Part 1 — A deterministic skill (formatter)
The simplest skill is a Python function with a typed contract. formatter takes
a name (plus an optional shout and couplet) and returns a text card. Its
skill.yaml is just the contract — no model, no SKILL.md:
title: Greeting Formatter
description: Lays out a small greeting card from a name.
entrypoint: scripts/format.py:run
input_schema:
type: object
required: [name]
properties:
name: { type: string, maxLength: 60 }
shout: { type: string, maxLength: 120 }
poem: { type: text, maxLength: 400 }
output_schema:
type: object
properties:
card: { type: text, description: The assembled greeting card. }
The entrypoint scripts/format.py:run points at a function. The worker calls it
as run(**inputs) and validates the returned dict against output_schema:
def run(name: str, shout: str = "", poem: str = "") -> dict:
headline = (shout or f"Hello, {name}!").strip()
# ...frame the headline + couplet in a text box...
return {"card": "\n".join(rows)}
That's a whole skill. A deterministic skill is code with a schema — use one
whenever the work is mechanical and needs no judgment. Pure stdlib here, so
there's no requirements.txt; add one next to the script if you need packages.
Part 2 — An agentic skill (greeter)
greeter does need judgment (writing a greeting), so it's agentic: its
entrypoint is SKILL.md and it declares a model. Its skill.yaml adds a
text_model, a richer input_schema, and a custom tool:
title: Greeter
entrypoint: SKILL.md
text_model: claude/haiku-4-5
input_schema:
type: object
required: [name]
properties:
name: { type: string, maxLength: 60 }
style: { type: string, enum: [friendly, formal, playful], default: friendly }
output_schema:
type: object
properties:
card: { type: text }
shout: { type: string }
poem: { type: text }
tools:
- name: emphasize
description: Make a short string LOUD.
entrypoint: tools/emphasize.py:run
input_schema:
type: object
required: [text]
properties:
text: { type: string }
output_schema:
type: object
properties:
loud: { type: string }
A few things to notice:
-
The model is set high-level, in
text_model:— not hard-coded in a prompt.greeteris a thin orchestrator, so it runsclaude/haiku-4-5; spend frontier models where real judgment lives. (See skill-yaml-reference for media model slots likeimage_model:too.) -
The schema dialect drives the UI.
style'senumbecomes a dropdown in the playground;type: textbecomes a multi-line field. Richer types (image,video,color) render upload widgets and pickers — a well-typed schema means the playground form "just works" with zero UI code. -
A custom tool is a deterministic Python function, declared under
tools:with its owninput_schema/output_schemaand afile.py:funcentrypoint — exactly like a deterministic skill, but callable by this agent:python# tools/emphasize.py def run(text: str) -> dict: return {"loud": text.upper().rstrip("!?. ") + "!!!"}
SKILL.md is the brain
skill.yaml is the contract; SKILL.md is the system prompt the agent runs.
greeter's walks through three small moves, then returns. It uses the tool and
two subagents rather than doing the work itself:
## Step 1 — Shout the name (custom tool)
emphasize({ "text": <name> }) → keep `loud` as your `shout`
## Step 2 — Ask the poet (a `.md` subagent)
run_subagent({ "target": "references/poet.md",
"inputs": { "name": <name>, "style": <style> } }) → keep `poem`
## Step 3 — Assemble the card (a sibling skill as a subagent)
run_subagent({ "target": "formatter",
"inputs": { "name": <name>, "shout": <shout>, "poem": <poem> } })
## Step 4 — Return
set_output({ "card": ..., "shout": ..., "poem": ... })
Subagents — two shapes
run_subagent hands a self-contained stage to a
fresh agent (its own context, linked to this run). greeter uses both forms,
resolved inside this same bundle:
- A
.mdprompt —target: "references/poet.md"runs that bundle file as the system prompt of an isolated subagent. Keep a stage's prompt inreferences/and point at it. This is the form a single skill uses to break itself into stages — no second skill required. - A sibling skill by name —
target: "formatter"runs another skill in the same bundle (this is the bit that needs a second skill alongsidegreeter). (Refs into another bundle useskillpack_slug/skillorworkspace/skillpack_slug/skill; an inlineprompt:runs a one-off with no file.)
inputs is passed to the child verbatim — it reads them as its own inputs. A
.md subagent lets one skill stay small by splitting its own work into stages;
calling a sibling skill is what turns a pack of skills into a pipeline.
Finishing a run — set_output
Every skill ends by calling set_output once, with exactly the fields in its
output_schema. For agentic skills it's an auto-injected tool; for deterministic
ones it's the returned dict. The platform validates the result against the schema
and prunes anything extra — so output_schema is the real boundary of what a
skill exposes.
Seed the playground with examples
Each skill.yaml carries an examples block — real inputs the playground loads
as one-click starting points. greeter ships a couple:
examples:
- title: Greet Ada
inputs: { name: Ada, style: playful }
- title: Greet the team (formal)
inputs: { name: the Puras team, style: formal }
Good examples are how a new user understands a skill in five seconds — invest in them, and make sure they mirror the real input schema.
Deploy and run it
Deploying is one command. Scaffold a skill and push it with the CLI — the
first puras deploy in a single-skill directory auto-creates the remote
target, so there's no separate create step:
pip install puras
puras init # scaffold a blank starter skill + puras.yaml
# (--template hello-world scaffolds this guide's example)
puras deploy # zip the dir + push a deployment (auto-created on first run)
puras run greeter --input name=Ada --input style=playful
puras.yaml is the manifest: it binds the directory to its remote deployment
(skillpack_id, slug) and carries the page's own title, description, and
optional marketing block — see skill-yaml-reference for the full shape.
To deploy to an explicit target, pass --app <id|slug> (the older
--skillpack flag is still accepted as an alias).
From here: cli-reference covers deploy / run / pull from your terminal,
mcp-tools covers the same from an agent plus running deployed skills, and
sdk-client-reference covers calling them from application code. To go deeper
on any field, the manifest spec is skill-yaml-reference; the agent's runtime
tools are agent-tools-reference; the in-skill Python runtime is
sdk-runtime-reference.
Advanced: shipping multiple skills (a pack)
Everything above deploys as a single skill. When several skills are related and should version and ship together — a pipeline where one calls the next — you bundle them as a skillpack: one deployable directory whose top-level folders are the skills.
That's exactly what hello-world is: greeter and formatter live side by side,
and greeter calls formatter as a sibling-skill subagent to lay out its card.
The two compose into a pipeline — a pack is a set of related skills, not a
junk drawer.
hello-world/ # the bundle root — a pack of two skills
├── greeter/ # each top-level dir with a skill.yaml is a skill
│ ├── skill.yaml
│ ├── SKILL.md
│ ├── tools/emphasize.py
│ └── references/poet.md
└── formatter/
├── skill.yaml
└── scripts/format.py
The rules are the same as one skill, just repeated: every top-level
<name>/skill.yaml is auto-discovered, the folder name is the skill name, and
there's no skills/ wrapper or root manifest. A deployment is one push of
the whole bundle, versioned together; activating a new one is a rolling
switch (new jobs use the active version; in-flight jobs finish on the version
they started). Adding a skill to a pack is just adding another top-level folder
and redeploying.
Once hello-world makes sense, the same four pieces — deterministic skills, an agent in the middle, custom tools, and subagents — scale from one skill up to any real multi-skill pipeline.