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/readalone → 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 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.
{
"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:
# 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-skillThis 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-skillAny 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-skillThese 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-localThen, in another terminal:
kx skills add --dir my-skillThe 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-triagekx 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:
| Check | What is refused |
|---|---|
schema | Anything other than kortecx.skill/v1 — readers fail closed on a mismatch. |
| Unknown fields | The shape is closed. A manifest cannot smuggle a new rail past validation. |
| Numbers | Any non-integer number, anywhere in the tree. |
name | Not 1–64 characters of [a-z0-9._-]; or not equal to the pack folder name. |
| Tool ids | Not one or two segments of [a-z0-9._-] — a/b/c, Gmail/Search, /x all fail. |
| Versions | Not an integer string — "latest" fails. |
| Size | A manifest over 64 KiB, or instructions over 256 KiB. |
instructions.md | Missing or empty. The instructions are the skill's semantic core. |
instructions_ref | Present 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
| Skill | Wishes | Posture |
|---|---|---|
research-summarize | retrieve@1, fs-read@1 | Grounded answers from retrieved passages and confined file reads; no connector needed. |
email-triage | gmail/search, gmail/read, gmail/draft | Draft, never send — sending stays a human act. |
channel-digest | discord/list_channels, discord/read_channel | Read-only; never posts. |
author-scheduled-app | gmail/search, gmail/read, gmail/draft, notion/search, notion/read_page, notion/create_page, slack/read_channel, discord/read_channel, retrieve | Helps 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-app | retrieve@1, fs-read@1 | Helps 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.
Connections (MCP)
Plug an outside service into the agentic runtime — name where it lives, and the runtime dials it, asks what it can do, and writes the answer down.
Authoring a Connector
Scaffold an MCP connector crate with one command, build it offline in fake mode, and check it against the same conformance gate the repository's CI runs.