kortecxdocs
Tools

Skills

A skill is a kortecx.skill/v1 pack — plain-prose instructions plus a wish-list of tools — that teaches an App how you want a job done without granting it any power.

A skill is how you write down the way you want a job done. It is a small folder with two things in it: instructions in plain prose ("read the inbox, classify what needs attention, draft replies, never send"), and a wish list of the tools that job expects to use.

You attach a skill to an App. The agent then reads your instructions as part of its own briefing, and the wished tools show up in its tool menu — if, and only if, you already had them.

That last clause is the whole design. A skill teaches behaviour. It never grants power.

A skill grants nothing

When you run an App that carries a skill, the agentic runtime works out what the agent actually gets by intersecting the wish with everything else that has to be true:

granted = wish ∩ your grants ∩ fireable-on-this-serve
  • wish — what the skill's manifest asked for.
  • your grants — the tool authority the calling identity already holds. A skill cannot widen it. Today this fold narrows only when the calling role carries explicit per-tool grants; a role that carries none is gated on "may this identity author at all" instead, and the other two folds still apply.
  • fireable-on-this-serve — tools this running server can genuinely call right now: registered in its tool registry, and for a connector, an actual reachable connection.

A wish that survives all three folds into the App's entry agentic step — the first model step that is a root of the blueprint graph — which is the same step your instructions bind to, so the agent always sees the instructions and the tools together.

A wish that does not survive is dropped with a warning, and the run continues with whatever could be granted. Nothing bricks. You get an honest, smaller agent rather than a failed launch.

The line that matters

If the intersection is empty, the step stays a plain transform with no tools at all. A skill on its own can never mint authority. The runtime has a test pinning exactly that: an empty fireable set grants nothing, no matter what the manifest wished for.

Worked through

Say you write a skill that wishes for gmail/search, gmail/read and gmail/draft, and you attach it to an App.

  • You have a working Gmail connection and the grants for all three → the agent can search, read and draft.
  • You have the connection but the server cannot dial it right now → all three drop with a warning. The run proceeds; the agent has your instructions and no Gmail tools, and should say so rather than invent results.
  • Someone else runs the same App under a role that carries explicit tool grants for gmail/read alone → they get read alone. Your skill did not lend them the other two.

Note what is not in that wish list: gmail/send. The reference pack deliberately never wishes for an irreversible send. The wish set is the hard line; the instructions are the soft one. If you want a human in front of an irreversible action anyway, see Approvals.

What a skill pack looks like

skill.json
instructions.md
README.md

skill.json is the kortecx.skill/v1 manifest. instructions.md is the know-how — the part that actually teaches. README.md is optional human docs. The folder name must equal the manifest name; the loader refuses a mismatch. A pack is exactly those three flat files — a fourth file, or a subdirectory, fails the conformance check below.

skills/email-triage/skill.json
{
  "schema": "kortecx.skill/v1",
  "name": "email-triage",
  "version": "1",
  "description": "Triage a Gmail inbox: search for actionable mail, read what matters, and DRAFT replies for human review — this skill deliberately never wishes for send.",
  "tags": ["email", "gmail", "triage"],
  "tools": {
    "gmail/draft": "1",
    "gmail/read": "1",
    "gmail/search": "1"
  }
}

Every key in tools is a tool id at a version. A bare id like retrieve is a capability the runtime bundles; a namespaced id like gmail/search is <connection-name>/<tool> from a connector.

The instructions are ordinary markdown. Here is the opening of the reference pack:

skills/email-triage/instructions.md
# Email triage

You triage the user's Gmail inbox. Your goal: surface what needs attention and
prepare replies — a human always reviews before anything is sent.

## Procedure

1. **Search narrowly.** Use `gmail/search` with a focused query …
2. **Read before judging.** For each candidate, use `gmail/read` …
3. **Draft, never send.** …

Build one

Scaffold the pack

kx new skill my-skill

This is offline — it contacts no server. It writes my-skill/skill.json, my-skill/instructions.md and my-skill/README.md with templates to fill in. Use --dir <parent> to put the pack somewhere other than the current folder.

Write the instructions and the wishes

Edit instructions.md — the role, the ordered procedure, the boundaries, the shape of the final answer. Then list the tools that procedure names in skill.json under tools.

Write the boundaries in both places. The prose says what the agent should not do; the wish set decides what it can ever touch.

Check the pack

The conformance harness is an example program in the runtime's source repository, so this step runs from a checkout of it. From the repository root:

just test-skill /path/to/my-skill

Any pack directory works, including one outside the repository's own skills/ folder. just test-skill is a thin wrapper; the harness underneath is:

cargo run -p kx-extension-sdk --example skill_conformance -- /path/to/my-skill

These are the same checks CI runs on the packs shipped in the repository. The server re-validates the pack when you add it, so this step is the faster loop rather than the only gate.

Add it to your catalog

You need a server running. On your own machine:

kx serve --dev-allow-local

Then, in another terminal:

kx skills add --dir my-skill

The pack is validated locally first, so a bad manifest fails with a readable error before anything is sent. The server re-runs the same validation, stores the instructions body, and derives the content refs itself — you never hand-type an identity.

Attach it to an App

kx app new triager --from-blueprint bp.json --skill email-triage
kx app run triager

--skill is repeatable. Each name is resolved against your catalog at authoring time; a name that is not there fails with a message telling you to run kx skills add first.

Managing the catalog

kx new skill my-skill
kx skills add --dir skills/email-triage
kx skills list
kx skills show --name email-triage
kx app new triager --from-blueprint bp.json --skill email-triage
kx skills remove --name email-triage

kx skills show prints the wish set with an advisory registered bit per tool — each line reads registered or UNREGISTERED — so you can see before you run which wishes this server could currently fill. That bit is display only; it is never itself a grant.

kx skills add also takes a manifest file instead of a pack directory: --manifest skill.json --instructions instructions.md. Pass --dir or --manifest, not both.

import kortecx as kx

with kx.KxClient() as client:
    client.skills.add(
        {
            "schema": "kortecx.skill/v1",
            "name": "research-summarize",
            "version": "1",
            "tools": {"retrieve": "1", "fs-read": "1"},
        },
        instructions="# Research\nRetrieve first. Cite what you read.",
    )
    for summary in client.skills.list():
        print(summary.name, summary.tools)

    app = (
        kx.app("researcher")
        .blueprint(kx.flow().agent("Answer the question from what you retrieve."))
        .skill(
            kx.Skill(
                name="research-summarize",
                instructions="# Research\nRetrieve first. Cite what you read.",
                tools={"retrieve": "1", "fs-read": "1"},
            )
        )
    )
    app.run(client=client)
import { KxClient, app, flow } from "@kortecx/sdk";

const kx = new KxClient();

await kx.skills.add({
  manifest: {
    schema: "kortecx.skill/v1",
    name: "research-summarize",
    version: "1",
    tools: { retrieve: "1", "fs-read": "1" },
  },
  instructions: "# Research\nRetrieve first. Cite what you read.",
});

const form = await kx.skills.show("research-summarize");
console.log(form?.wishes);

await app("researcher")
  .blueprint(flow().agent("Answer the question from what you retrieve."))
  .skill({
    name: "research-summarize",
    instructions: "# Research\nRetrieve first. Cite what you read.",
    tools: { retrieve: "1", "fs-read": "1" },
  })
  .run({}, { client: kx });

What validation refuses

A skill manifest is checked fail-closed — if a check cannot be satisfied, the manifest is rejected rather than accepted with the doubtful part ignored.

No authority may appear in a manifest, anywhere

Any object key containing warrant, grant, secret, credential or executable is refused at any depth in the manifest — toolGrants, client_secret, awsCredentials all fail the same way. A skill wishes; the server grants. There is no key you can add to change that.

The other refusals:

CheckWhat is refused
schemaAnything other than kortecx.skill/v1 — readers fail closed on a mismatch.
Unknown fieldsThe shape is closed. A manifest cannot smuggle a new rail past validation.
NumbersAny non-integer number, anywhere in the tree.
nameNot 1–64 characters of [a-z0-9._-]; or not equal to the pack folder name.
Tool idsNot one or two segments of [a-z0-9._-]a/b/c, Gmail/Search, /x all fail.
VersionsNot an integer string — "latest" fails.
SizeA manifest over 64 KiB, or instructions over 256 KiB.
instructions.mdMissing or empty. The instructions are the skill's semantic core.
instructions_refPresent in a pack manifest. The server derives it when it stores the body; you never write it by hand.

A few wishes can also be dropped at run time for reasons that are not about authority at all: a tool with no definition in the server's registry, a tool whose sandbox syscall profile cannot share the step's warrant, a tool whose filesystem scope is incomparable with the other tools on that step, and anything past the cap of 16 tools on one folded contract. Tools declared directly in the blueprint always win over a skill's wish for the same id, and are never evicted by one.

The packs that ship with the runtime

SkillWishesPosture
research-summarizeretrieve@1, fs-read@1Grounded answers from retrieved passages and confined file reads; no connector needed.
email-triagegmail/search, gmail/read, gmail/draftDraft, never send — sending stays a human act.
channel-digestdiscord/list_channels, discord/read_channelRead-only; never posts.
author-scheduled-appgmail/search, gmail/read, gmail/draft, notion/search, notion/read_page, notion/create_page, slack/read_channel, discord/read_channel, retrieveHelps you author a scheduled App. Reads, plus two reversible writes — a Gmail draft and a new Notion page; it never wishes for an irreversible send.
author-hosted-appretrieve@1, fs-read@1Helps you author a hosted App; read-only wishes.

Each one is gated by the same conformance harness you run on your own pack, and each demonstrates the same boundary: the instructions say what to do, the wish set is the hard limit on what can ever be touched, and your grants plus the live server decide what is actually handed over.

If a wish is `retrieve`

Retrieval reads from a dataset, and a dataset needs an hnsw build before it can be searched. See Datasets. Without one, a retrieve wish has nothing to read even when it is granted.

What a skill is not

  • Not code. A manifest carries markdown and a map of tool ids. The executable part is always a separate out-of-process connector or a capability the runtime bundles.
  • Not a credential. Secrets live in Secrets and connections in Connections. A skill only ever names a tool.
  • Not a blueprint. It attaches to an App you already have; it does not define the graph. For the graph, see Workflows.