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 deploypuras 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:

SkillKindEntrypointWhat it shows
greeteragenticSKILL.mda custom tool + two subagents, driven by a model
formatterdeterministicscripts/format.py:runa 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:

yaml
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:

python
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:

yaml
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. greeter is a thin orchestrator, so it runs claude/haiku-4-5; spend frontier models where real judgment lives. (See skill-yaml-reference for media model slots like image_model: too.)

  • The schema dialect drives the UI. style's enum becomes a dropdown in the playground; type: text becomes 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 own input_schema/output_schema and a file.py:func entrypoint — 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:

markdown
## 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 .md prompttarget: "references/poet.md" runs that bundle file as the system prompt of an isolated subagent. Keep a stage's prompt in references/ 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 nametarget: "formatter" runs another skill in the same bundle (this is the bit that needs a second skill alongside greeter). (Refs into another bundle use skillpack_slug/skill or workspace/skillpack_slug/skill; an inline prompt: 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:

yaml
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:

bash
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.